Tuesday, October 6, 2009 3 comments

JBoss Caching Integration

JBoss caching is a very powerful caching mechanism developed and as far as i know this is the only Tree Cache implementation currently available. If i give a briefing on what caching is all about, caching allows you to minmize the time you go to the database to fetch data. We all know how costly database access is, and caching allows us to minmize those calls by caching the results fetched from the database. Ofcourse a general rule of thumb in caching is that you should always consider caching data that rarely change but frequently access.

There are many caching implementations out there, some being OSCache, EHCache, Teracotta (if im not mistaken this does allow caching as well) and many more are out there. I havent used these, but was involved in integrating OSCache at one point. OSCache is pretty much ok if your application is running on a single instance. When it comes to clustering, it becomes a bit complicated to configure OSCache. But still it is possible by the usage of Multicast addresses to publish cached data to multiple clusters but it is awfully complicated. And OSCache does not get involved in Transactions as far as i can remember. This is where JBoss caching comes in.

JBoss caching seamlessly integrated into a clustered environment with only a few configurations and most importantly it gets involved in Transactions and hence it only caches the data if the transaction successfully committs. And if your entities are updated within a transaction, so will the data in the cache be updated and works the same if rows are deleted from the database. Following I will show you how to integrate JBoss caching to your JBoss AS. I wanted to publish this article because although there was alot of documentation availabel on caching(JBoss does have comprehensive documentation) the information was dispersed so my intention of this article is to provide step by step operations to integrate JBoss caching. Following shows how to use JBoss caching with Hibernate.

Before i start explaining how to integrate JBoss caching there are some details you need to be aware of.

  1. JBossCache versions prior to 1.2.2 are not recommended for use with Hibernate, due to deadlock issues that may arise.
  2. Hibernate versions prior to 3.0.2 are not recommended for use with JBossCache, for the same deadlock issues mentioned above.
  3. Hibernate >= 3.2 is required if you plan to use Optimistic Locking. JBoss Cache >= 3.0.0 and Hibernate >= 3.3.0 is your optimal configuration, using MVCC as a locking scheme on JBoss Cache.
Im using Jboss cache 1.4.1 with Hibernate 3.2 so that i can use the Optimistic Locking feature provided.

The above list was taken from the Jboss wiki. Please have a look at the configuration table provided in that too to find the optimal configuration suited for you.

First of all you need to get the jboss caching distribution. Extract the jboss-cache.jar and jgroups.jar from that distribution as thats all you need. Im currently using jboss caching 1.4.1 with JBoss AS 4.2.2 GA. Place these two jars withing your deployment lib folder. For example /yourjbosslocation/server/default/lib. If you are using the "all" distribution the jgroups.jar will already be available to you. But in production you will almost always have your won deployment configuration.

Afterwards you need to define your caching implentation to be loaded as a service as MBeans. The following is the xml file ejb3-entity-cache-service.xml which you can find in your "all" configuration. The following file is edited a bit to include optimistic features.


<?xml version="1.0" encoding="UTF-8"?>
<server>

<!-- ============================================================ -->
<!-- Clustered entity cache config for use with JBoss Cache 1.4.x -->
<!-- ============================================================ -->
<mbean code="org.jboss.cache.TreeCache"
name="jboss.cache:service=EJB3EntityTreeCache">

<depends>jboss:service=Naming</depends>
<depends>jboss:service=TransactionManager</depends>

<!-- Name of cluster. Needs to be the same on all nodes in the clusters,
in order to find each other -->
<attribute name="ClusterName">${jboss.partition.name:DefaultPartition}-EntityCache</attribute>

<!-- Configure the TransactionManager -->
<attribute name="TransactionManagerLookupClass">org.jboss.cache.JBossTransactionManagerLookup</attribute>

<!--
Node locking level : SERIALIZABLE
REPEATABLE_READ (default)
READ_COMMITTED
READ_UNCOMMITTED
NONE
-->
<attribute name="IsolationLevel">REPEATABLE_READ</attribute>


<!--
Note that this will ignore the Isolation Level parameter if OPTIMISTIC is used
Node locking scheme:
OPTIMISTIC
PESSIMISTIC (default)
-->
<attribute name="NodeLockingScheme">OPTIMISTIC</attribute>


<!-- Valid modes are LOCAL
REPL_ASYNC
REPL_SYNC
-->
<attribute name="CacheMode">REPL_SYNC</attribute>

<!-- Must be true if any entity deployment uses a scoped classloader -->
<attribute name="UseRegionBasedMarshalling">true</attribute>
<!-- Must match the value of "useRegionBasedMarshalling" -->
<attribute name="InactiveOnStartup">true</attribute>

<!--
JGroups protocol stack config in XML format.

On Windows machines, because of the media sense feature
being broken with multicast (even after disabling media sense)
set the UDP.loopback attribute to true
-->
<attribute name="ClusterConfig">
<config>
<UDP mcast_addr="${jboss.partition.udpGroup:230.1.2.3}"
mcast_port="${jboss.ejb3entitypartition.mcast_port:43333}"
tos="8"
ucast_recv_buf_size="20000000"
ucast_send_buf_size="640000"
mcast_recv_buf_size="25000000"
mcast_send_buf_size="640000"
loopback="false"
discard_incompatible_packets="true"
enable_bundling="false"
max_bundle_size="64000"
max_bundle_timeout="30"
use_incoming_packet_handler="true"
use_outgoing_packet_handler="false"
ip_ttl="${jgroups.udp.ip_ttl:2}"
down_thread="false" up_thread="false"/>
<PING timeout="2000"
down_thread="false" up_thread="false" num_initial_members="3"/>
<MERGE2 max_interval="100000"
down_thread="false" up_thread="false" min_interval="20000"/>
<FD_SOCK down_thread="false" up_thread="false"/>
<FD timeout="10000" max_tries="5" down_thread="false" up_thread="false" shun="true"/>
<VERIFY_SUSPECT timeout="1500" down_thread="false" up_thread="false"/>
<pbcast.NAKACK max_xmit_size="60000"
use_mcast_xmit="false" gc_lag="0"
retransmit_timeout="300,600,1200,2400,4800"
down_thread="false" up_thread="false"
discard_delivered_msgs="true"/>
<UNICAST timeout="300,600,1200,2400,3600"
down_thread="false" up_thread="false"/>
<pbcast.STABLE stability_delay="1000" desired_avg_gossip="50000"
down_thread="false" up_thread="false"
max_bytes="400000"/>
<pbcast.GMS print_local_addr="true" join_timeout="3000"
down_thread="false" up_thread="false"
join_retry_timeout="2000" shun="true"
view_bundling="true"/>
<FRAG2 frag_size="60000" down_thread="false" up_thread="false"/>
<pbcast.STATE_TRANSFER down_thread="false" up_thread="false" use_flush="false"/>
</config>
</attribute>

<!-- The max amount of time (in milliseconds) we wait until the
initial state (ie. the contents of the cache) are retrieved from
existing members.
-->
<attribute name="InitialStateRetrievalTimeout">17500</attribute>

<!-- Number of milliseconds to wait until all responses for a
synchronous call have been received.
-->
<attribute name="SyncReplTimeout">17500</attribute>

<!-- Max number of milliseconds to wait for a lock acquisition -->
<attribute name="LockAcquisitionTimeout">15000</attribute>

<!-- Name of the eviction policy class. -->
<attribute name="EvictionPolicyClass">org.jboss.cache.eviction.LRUPolicy</attribute>

<!-- Specific eviction policy configurations. This is LRU -->
<attribute name="EvictionPolicyConfig">
<config>
<attribute name="wakeUpIntervalSeconds">5</attribute>
<!-- Cache wide default -->
<region name="/_default_">
<attribute name="maxNodes">5000</attribute>
<attribute name="timeToLiveSeconds">1000</attribute>
</region>
<!-- Caching region for Seating related entities. Will remain for 2hours before eviction -->
<region name="/mycache/Seating">
<attribute name="maxNodes">10000</attribute>
<attribute name="timeToLiveSeconds">7200</attribute>
</region>

</config>
</attribute>

</mbean>

</server>



The above file needs to be put in your "deploy" folder so that the caching service can find the configuration. In the above you can see I have used the "NodeLockingScheme" attribute and set it to "OPTIMISTIC". This is what allows for OptimisticLocking to take palce. Note that this will only work if you are using Hibernate 3.2. Else you should use the "IsolationLevel" attribute. In this same file i have defined that attribute as well but as i have used the "NodeLockingScehem" attribute, which overrides the "IsolationLevel" attribute. And if you look at the "CacheMode" attribute i have set it to "REPL_SYNC". If you look at the JBoss wiki link i have given above it states the following;


  1. If you are only using a query cache or collection cache, use REPL_ASYNC.

  2. If you are only caching entities, use INVALIDATION_SYNC

  3. If you are using a combination of query caching and entity caching, use REPL_SYNC.



Hence my use case is that i will be using query and collection cache as well as entity cache and hence REPL_SYNC is the ideal configuration.

Afterwards i want to move your focus to the region tags specified within the xml configuration above. If you only keep the default region then what happens is all the caching will be stored in one specifc region. If its entity caching then it will be stored in the default region with the fully qualified class name. Hence it is always best to separate your caching regions on entity basis or whatever basis you deem appropriate. One thing to keep in mind is to provide a meaningul eviction time as you do not want to keep your data forever in the cache. I have defined one caching region in the above configuration. I will next show you how to map this region name in your entity class. Below i give you the entity bean class which shows how to enable caching at the entity level.


package com.test.domain.seat;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.QueryHint;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;



/***********************************************************************************************************************
* Description : Seat Occupancy
*
* @author Dinuka
**********************************************************************************************************************/
@Entity
@Table(name = " SEAT_OCCUPANCY")
public class SeatOccupancy extends Persistent implements Serializable {

private static final long serialVersionUID = -4339488117889231833L;

@Id
@SequenceGenerator(name = "SEAT_OCCUPANCY", sequenceName = "SEAT_OCCUPANCY")
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEAT__SEQ_OCCUPANCY")
@Column(name = "SEAT_OCCUPANCY_ID")
private Long SeatOccupancyId;


@Cache (usage=CacheConcurrencyStrategy.TRANSACTIONAL,region="Seating")
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "SEAT_OCCUPANCY_CHAR", joinColumns = { @JoinColumn(name = "SEAT_OCCUPANCY_ID") }, inverseJoinColumns = { @JoinColumn(name = " CHARACTERISTIC_CODE") })
@BatchSize(size=10)
private List<SeatCharacteristic> seatCharacteristic = new ArrayList<SeatCharacteristic>();


@OneToMany(mappedBy = "seatOccupancy", cascade = CascadeType.ALL)
@org.hibernate.annotations.Cascade(value=org.hibernate.annotations.CascadeType.DELETE_ORPHAN)
@BatchSize(size=10)
private List<SeatOccupancyDetail> seatOccupancyDetail = new ArrayList<SeatOccupancyDetail>();



/**
* @return the seatOccupancyId
*/
public Long getSeatOccupancyId() {
return SeatOccupancyId;
}

/**
* @param seatOccupancyId the seatOccupancyId to set
*/
public void setSeatOccupancyId(Long seatOccupancyId) {
SeatOccupancyId = seatOccupancyId;
}



/**
* @return the seatCharacteristic
*/
public List<SeatCharacteristic> getSeatCharacteristic() {
return seatCharacteristic;
}

/**
* @param seatCharacteristic the seatCharacteristic to set
*/
public void setSeatCharacteristic(List<SeatCharacteristic> seatCharacteristic) {
this.seatCharacteristic = seatCharacteristic;
}

/**
* @return the seatOccupancyDetail
*/
public List<SeatOccupancyDetail> getSeatOccupancyDetail() {
return seatOccupancyDetail;
}

/**
* @param seatOccupancyDetail the seatOccupancyDetail to set
*/
public void setSeatOccupancyDetail(List<SeatOccupancyDetail> seatOccupancyDetail) {
this.seatOccupancyDetail = seatOccupancyDetail;
}


}


Here i have done a collection caching. You need to defined the usage and the region. If you do not specify the region it will be stored in the default region as i mentioned before. You may have noted that i have only said the region name to be "Seating" but in the configuration file which was shown before it said "/mycache/Seating". If you are caching a collection of objects please note that you should define that specifc entity as cacheable too. This brings us to the next topic which defines an entity to be cacheable.


@Entity
@Cache(usage=CacheConcurrencyStrategy.TRANSACTIONAL,region="Seating")
@Table(name = "SEAT_CHARACTERISTIC")
public class SeatCharacteristic implements Serializable {

.....

}


And if you want to use query caching here is how you would go about doing it.


//For Named Queries
@NamedQuery(name = "findAllSeats", query = "from SeatOccupancy", hints = { @javax.persistence.QueryHint(name = "org.hibernate.cacheable", value = "true") }),

//For Normal Queries
Query allSeats = session.createQuery("from SeatOccupancy so");
bandsByName.setCacheable(true);


One thing to note here is that if you are using the "JOIN FETCH" statement in your queries do not cache those as the caching wont work for those kind of queries.

Lastly there is one last step to compelte the integration. You need to tell hibernate that you want to enable 2nd level caching and that you are going to be using the JBoss caching implementation. Provide the following parameters in your persistence.xml file or .hbm file.


<properties>

<property name="hibernate.transaction.manager_lookup_class" value="org.hibernate.transaction.JBossTransactionManagerLookup"/>
<!-- Clustered cache with TreeCache -->
<property name="cache.provider_class" value="org.jboss.ejb3.entity.TreeCacheProviderHook"/>
<property name="treecache.mbean.object_name" value="jboss.cache:service=EJB3EntityTreeCache"/>
<property name="hibernate.cache.region_prefix" value="mycache"/>
<property name="hibernate.cache.use_query_cache" value="true"/>
<property name="hibernate.cache.use_second_level_cache" value="true"/>
<property name="hibernate.cache.provider_class" value="org.jboss.ejb3.entity.TreeCacheProviderHook"/>
<property name="hibernate.treecache.mbean.object_name" value="jboss.cache:service=EJB3EntityTreeCache"/>
</properties>


Now i come back to the part of why we only defined the region name in the previous entity as only "Seating". Its because we have given the "hibernate.cache.region_prefix" property the value "mycache" and hence the region will always be relative to "/mycache". And of course you need to tell hibernate to enable second level and query caching which is stated above. And the other properties are self explaining as far as i can see.

Well folks, thats about it on how to integrate JBoss caching to your Jboss AS. If you come acorss any difficulties please do contact me as i would very much like to help you.
 
;