`
一江春水邀明月
  • 浏览: 77569 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Hibernate 二级缓存工作逻辑与Hibernate-memcached 对Read-Write Cache的支持原理初探

 
阅读更多

版权所有, 转载请提供原始地址http://wangbt5191-hotmail-com.iteye.com/admin/blogs/1711129

 

最近要给公司的某产品的hibernate-jbosscache 方案做架构的整理, 本人是力主转到hibernate-memcached 方案的, 故阅读了hibernate cache 相关的代码,这里总结一下。

 

Hibernate 二级缓存的四种级别

Read-only

  1. 对应Hibernate 的类是 org.hibernate.cache.ReadOnlyCache
  2. 只支持Cache Object的 Read/Create, 不支持Cache Object的 Update/delete 操作, 更不支持锁操作
  3. 性能最好

Nonstrict read-write

  1. 对应Hibernate 的类是 org.hibernate.cache.NonstrictReadWriteCache
  2. 不支持Cache Object的锁操作, 所以可能有脏读的问题
  3. 但是它提供Cache 清楚机制, 所以后续的读操作可以从DB 中读取到正确的对象

Read-Write

  1. 对应Hibernate 的类是 org.hibernate.cache.ReadWriteCache
  2. 以SoftLock 对Cache Object 进行锁定, 具体实现后文中会有具体描述

Transactional

  1. 对应Hibernate 的类是 org.hibernate.cache.TransactionalCache,
  2. 明显事务级别的Cache的更新DB和Cache 是在同一个事务中进行的, 它的一致性最好
  3. 在TransactionalCache 类中并没有多少对Transactional 的控制, 它的具体实现都是推迟到具体的Cache 实现中的

Hibernate 中如何指定Cache 级别

我们可以有两种方式指定Cache级别

1. 在persistence.xml中
<property  name="hibernate.ejb.classcache.com.best.oasis.genidc.biz.system.model.SysUserOfOrg"
				value="transactional" />
 
2. 在hibernate entity POJO 中

  在entity POJO 类上加annotation 声明

 

  @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
 
3.Notes:

  需要注意的是在实际项目中, 我们的配置所能获得的Cache 级别还和所使用的Cache Provider 实现了哪个级别有关, 比如, 如果你使用Ehcache, 它只支持read-write|nonstrict-read-write|read-only 这几个级别, 那么你无论如何都无法获得Transactional级别的Cache的。

为什么我们用Read-Write 级别就够用了

Read-Write可以获得Cache 上事务的 repeatable read 隔离级别

Repeatable Read的含义:

在单个事务中,即使外界有其他事务修改对象值, 当前事务开始的时候获取的值和后续获得的值总是相同的, 如果你在Session1 中获取了一个value, 然后在Session2 中Update, 然后在Session 中重新读取, 获得的结果是Session1 刚刚开始的时候获取到的那个值, 换一种说法就是读操作在一个会话里面是可重复的。
我们以DB为例.

 

session1> BEGIN;
session1> SELECT firstname FROM names WHERE id = 7;
Aaron
session2> BEGIN;
session2> SELECT firstname FROM names WHERE id = 7;
Aaron
session2> UPDATE names SET firstname = 'Bob' WHERE id = 7;
session2> SELECT firstname FROM names WHERE id = 7;
Bob
session2> COMMIT;
session1> SELECT firstname FROM names WHERE id = 7;
Aaron

 Read Commit的含义:

在事务的上下文中, 我们总能够获取到最近提交的事务的值.
我们还是以DB为例

session1> BEGIN;
session1> SELECT firstname FROM names WHERE id = 7;
Aaron
session2> BEGIN;
session2> SELECT firstname FROM names WHERE id = 7;
Aaron
session2> UPDATE names SET firstname = 'Bob' WHERE id = 7;
session2> SELECT firstname FROM names WHERE id = 7;
Bob
session2> COMMIT;
session1> SELECT firstname FROM names WHERE id = 7;
Bob

 

Transactional Cache的代价

Hibernate为了获取到Cache的事务特性,Cache 需要保存什么时间谁获取了什么资源的硬锁信息, 并且加锁解锁过程需要同步和协调, 它的问题在于:

1. 占用资源

2. 对集群横向扩展不友好

3. 死锁问题导致系统不稳定

Hibernate ReadWriteCache 主要接口

CacheConcurrencyStrategy接口

Hibernate提供了CacheConcurrencyStrategy接口定义了Hibernate L2 Cache的行为, 它更具体的文档可以参考这里的Hibernate API 文档。 这里要特别注意的是Hibernate 对Cache的操作顺序

  1. Entity Read:     transaction begin -> get() --> transaction end - -> put()
  2. Entity Delete: transaction begin --> lock()  --> DB操作   -- > evict () -->  transaction end ---> release()
  3. Entity Update:transaction begin -> lock()- >- DB操作  - >  update()-> transaction end - -> afterUpdate()
  4. Entity Inserts: transaction begin -> insert() -> transaction end ---> afterInsert()
  5. 特别要注意的是如果要是Cache中的entry 失效, 我们的调用顺序是 Lock()-> evict() -> release()

CacheConcurrencyStrategy 在Hibernate 中的实现类有上文提到的ReadOnlyCache, NonstrictReadWriteCache, ReadWriteCache, TransactionalCache。

这里需要注意的是我们这里的接口release(), afterUpdate(), afterInsert() 是在事务之外回调的。这个我们后面会更详细的探讨。

ReadWriteCache实现

ReadWriteCache 它在Hibernate API的官方文档 中是这么说明的  (翻译的不够好, 英文好的童鞋还是直接看原文的javadoc 吧)

Cache数据优势在更新的时候是能够保持“Read Commmitted” 语义的隔离级别的。 如果DB设置成“repeatable read”隔离级别, 同步策略会折中获得Repeatable read的Cache 隔离级别。

在集群中, 底层的cache 实现需要支持分布式硬锁定, 这个策略也会假定底层实现的cache 在锁释放的时候不会进行异步的同步和对状态的全复制。(后文的分析Memcached 我们可以发现Memcached 恰好可以规避掉这个问题)

MemcachedCache

在CacheConcurrencyStrategy的各实现类中, 我们都可以看到Cache接口身影它 定义了最基本的Cache的增CRUD以及Lock等的操作, 它的具体底层实现类有   EhCache, HashtableCache, OptimisticTreeCache, OSCache, SwarmCache, TreeCache, 当然也有我们的MemcachedCache类。

对Hibernate  ReadWriteCache 对Cache的设计

这里我们先做点准备工作介绍下ReadWriteCache 中重要的内部接口和一个内部类以及他们的几个重要的方法.

public static interface Lockable {
		public Lock lock(long timeout, int id);
		public boolean isLock();
		public boolean isGettable(long txTimestamp);
		public boolean isPuttable(long txTimestamp, Object newVersion, Comparator comparator);
	}
public static final class Item implements Serializable, Lockable {

		public Item(Object value, Object version, long currentTimestamp) {
			this.value = value;
			this.version = version;
			freshTimestamp = currentTimestamp;
		}

		public boolean isGettable(long txTimestamp) {
			return freshTimestamp < txTimestamp;
		}

		/**
		 * Don't overwite already cached items
		 */
		public boolean isPuttable(long txTimestamp, Object newVersion, Comparator comparator) {
			// we really could refresh the item if it
			// is not a lock, but it might be slower
			//return freshTimestamp < txTimestamp
			return version!=null && comparator.compare(version, newVersion) < 0;
		}
	}
 public static final class Lock implements Serializable, Lockable, SoftLock {
		public Lock(long timeout, int id, Object version) {
			this.timeout = timeout;
			this.id = id;
			this.version = version;
		}
		/**
		 * Can the timestamped transaction re-cache this
		 * locked item now?
		 */
		public boolean isPuttable(long txTimestamp, Object newVersion, Comparator comparator) {
			if (timeout < txTimestamp) return true;
			if (multiplicity>0) return false;
			return version==null ?
				unlockTimestamp < txTimestamp :
				comparator.compare(version, newVersion) < 0; //by requiring <, we rely on lock timeout in the case of an unsuccessful update!
		}

		/**
		 * locks are not returned to the client!
		 */
		public boolean isGettable(long txTimestamp) {
			return false;
		}

	}

 在ReadWriteCache 中, 一个entity 的Key 在某一个时刻写在底层L2 Cache实现中的要么是Lock 实例, 要么是Item 实例。 我们一定要牢记这点, 这是理解ReadWriteCache数据一致性的关键。

一. 竞争读的分析

这一节我们讨论多个读操作同时发生的时候WriteReadCache 和底层Cache 实现发生了什么事情

ReadWriteCache Put, Get 操作

public synchronized boolean put(
			Object key,
			Object value,
			long txTimestamp,
			Object version,
			Comparator versionComparator,
			boolean minimalPut)
	throws CacheException {
		try {
			cache.lock(key);
			Lockable lockable = (Lockable) cache.get(key);
			boolean puttable = lockable==null ||
				lockable.isPuttable(txTimestamp, version, versionComparator);
			if (puttable) {
				cache.put( key, new Item( value, version, cache.nextTimestamp() ) );
				return true;
			}
			else {
				return false;
			}
		}
		finally {
			cache.unlock(key);
		}
	}

public synchronized Object get(Object key, long txTimestamp) throws CacheException {
			Lockable lockable = (Lockable) cache.get(key);

			boolean gettable = lockable!=null && lockable.isGettable(txTimestamp);

			if (gettable) {
				return ( (Item) lockable ).getValue();
			}
			else {
				return null;
			}
	}

 

ReadWriteCache Put,Get分析

这里的 cache.lock(key); 和cache.unlock(key); 可以无视, 因为MemcachedCache中对cache 的实现中, 这两个接口方法的实现就是两个空方法而已, 什么都没做。 重点要看它到底放了什么对象到实际的Cache中去了cache.put( key, new Item( value, version, cache.nextTimestamp() ) );
我们可以看到它放的是一个Item 是实现Lockable接口 的包装对象, 它有三个部分, 实际需要存Cache的payload:value, 版本号version 和cache.nextTimestamp()在MemcachedCache 的实现是

 public long nextTimestamp() {
        return System.currentTimeMillis() / 100;
    }

 这个类时间戳的用途将在get 操作中要用到, 这里先略过。

这里重点看这段

 

boolean puttable = lockable==null ||
     lockable.isPuttable(txTimestamp, version, versionComparator);

 从这段我们看到, 它的逻辑是

1. 如果从cache 中读出来的是空的, 那么我们需要把value 的包装对象放到cache 中
2. 那么Item 实现类中的isPuttable 是如何定义的呢?哈哈, 原来它是拿当前version 和已经在cache 中存的Item 的version 做相比, 说穿了就是如果当前版本比cache 中存的对象的版本大, 那么我们就应该重新存cache 了。原来是用到乐观锁。

这里我们来看一下Item类的几个重要的方法

 

	public boolean isGettable(long txTimestamp) {
			return freshTimestamp < txTimestamp;
		}

 

上面我们在分析put 的时候知道我们存cache 里面的包装对象Item 是包含 实际需要存Cache的payload:value, 版本号version和一个timestamp, 我们也知道这个timestamp 取的是当前绝对时间与100 的取整。 boolean gettable = lockable!=null && lockable.isGettable(txTimestamp);
我们来看一下这个gettable 的含义:
1. 如果从二级缓存中取不到, null, 那么就返回null, 从db 中去拿新值, 这个好理解
2. Item 实现类中的isGettable是如何定义的呢, 原来就是对比时间戳, 当然这个时间戳有个100ms 的误差容忍, 说穿了也是一个乐观锁的实现

 

	public boolean isGettable(long txTimestamp) {
			return freshTimestamp < txTimestamp;
		}

 具体在程序运行中就是, 线程从memcached 中获取对象, 查看这个对象当时存的时间点freshTimestamp, 如果当时的cache存取时间已经比现在事务的时间还要大(也就是cache 的存的时间+100Ms 误差 > 我们的当前时间),我们可以认为当前cache 的数据是不太确定的, 那么不允许线程从二级缓存中拿数据, 返回null, 让hibernate 从db 中load 数据;

ReadWriteCache 并发DB read 对Cache 一致性的分析:

通过前文我们知道Hibernate 做entity read 的时候, 有L2 Cache参与的情况下, entity read 操作只和ReadWriteCache的get 和 put 打交道;

通过阅读ReadWriteCache 读写逻辑, 我们大致可以判断如果我们的DB使用read  committed 隔离级别, 那么我们cache 里面的数据可以得到类read  committed 的隔离级别;  如果DB使用repeatable read 级别, 那么我们的cache 将得到repeatable read 级别的隔离;

二. 竞争写的分析

这一节我们讨论多个写操作同时发生的时候WriteReadCache 和底层Cache 实现发生了什么事情

前文我们提到过, Hibernate 在做entity 的写操作,它对CacheConcurrencyStrategy 的调用是这样的

  1. Entity Delete: transaction begin --> lock()  --> DB操作   -- > evict () -->  transaction end ---> release()
  2. Entity Update:transaction begin -> lock()- >- DB操作  - >  update()-> transaction end - -> afterUpdate()
  3. Entity Inserts: transaction begin -> insert() -> transaction end ---> afterInsert()

其中release, afterUpdate, afterInsert 是在事务外进行的, lock, evict (), update(), insert() 在事务内进行的。

我们先看在ReadWriteCache 里面的实现, 我们可以看到evict, insert, update 在ReadWriteCache 的实现里面都是不做任何事情的, 所以忽略不计;

那么剩下来我们需要重点看lock, release,  afterUpdate, afterInsert干了什么

 

public synchronized SoftLock lock(Object key, Object version) throws CacheException {
		try {
			cache.lock(key);
			Lockable lockable = (Lockable) cache.get(key);
			long timeout = cache.nextTimestamp() + cache.getTimeout();
			final Lock lock = (lockable==null) ?
				new Lock( timeout, nextLockId(), version ) :
				lockable.lock( timeout, nextLockId() );
			cache.update(key, lock);
			return lock;
		}
		finally {
			cache.unlock(key);
		}

	}
	private int nextLockId() {
		if (nextLockId==Integer.MAX_VALUE) nextLockId = Integer.MIN_VALUE;
		return nextLockId++;
	}

 public synchronized void release(Object key, SoftLock clientLock) throws CacheException {
		try {
			cache.lock(key);
			Lockable lockable = (Lockable) cache.get(key);
			if ( isUnlockable(clientLock, lockable) ) {
				decrementLock(key, (Lock) lockable);
			}
			else {
				handleLockExpiry(key);
			}
		}
		finally {
			cache.unlock(key);
		}
	}
	private void decrementLock(Object key, Lock lock) throws CacheException {
		//decrement the lock
		lock.unlock( cache.nextTimestamp() );
		cache.update(key, lock);
	}
	void handleLockExpiry(Object key) throws CacheException {
		long ts = cache.nextTimestamp() + cache.getTimeout();
		// create new lock that times out immediately
		Lock lock = new Lock( ts, nextLockId(), null );
		lock.unlock(ts);
		cache.update(key, lock);
	}

  public synchronized boolean afterUpdate(Object key, Object value, Object version, SoftLock clientLock)
	throws CacheException {
		try {
			cache.lock(key);
			Lockable lockable = (Lockable) cache.get(key);
			if ( isUnlockable(clientLock, lockable) ) {
				Lock lock = (Lock) lockable;
				if ( lock.wasLockedConcurrently() ) {
					// just decrement the lock, don't recache
					// (we don't know which transaction won)
					decrementLock(key, lock);
					return false;
				}
				else {
					//recache the updated state
					cache.update( key, new Item( value, version, cache.nextTimestamp() ) );
					return true;
				}
			}
			else {
				handleLockExpiry(key);
				return false;
			}
		}
		finally {
			cache.unlock(key);
		}
	}
public synchronized boolean afterInsert(Object key, Object value, Object version)
	throws CacheException {
		try {
			cache.lock(key);
			Lockable lockable = (Lockable) cache.get(key);
			if (lockable==null) {
				cache.update( key, new Item( value, version, cache.nextTimestamp() ) );
				return true;
			}
			else {
				return false;
			}
		}
		finally {
			cache.unlock(key);
		}
	}

 这里逻辑搞了这么多, 其实我们只要记住几条:

1. 在事务里面, hibernate 调用了lock 接口, 具体在ReadWriteCache 中, 就是往底层cache 实现中更新<Key, Lock>  的键值对, 注意, 如果以前这个Key对应的是Item, 那么这个时候会把它覆盖。

其中Lock 有预计超时时间点, lockid, entity 对象版本号三个属性;

2. 在事务外hibernate 调用 afterInsert接口的时候, ReadWriteCache 往底层cache 实现中更新 <Key, Item>  的键值对;

3. 在事务外hibernate 调用 release接口的时候ReadWriteCache往底层cache 中更新<Key, Lock>, 它的作用是对Lock的超时时间进行更新, 缩减或者延长;

4.  在事务外hibernate 调用 release afterUpdate的时候, ReadWriteCache根据锁状态, 要么把锁时间缩减, 就是更新<Key, Lock>;  要么是更新<Key, Item> 进去;

三.读写竞争的分析

这一节我们讨论写和读操作同时发生的时候WriteReadCache 和底层Cache 实现发生了什么事情

通过前面的分析我们可以得到如下结论:

1. read entity 的时候如果拿到的键值对的值是一个Lock, 那么Lock 上的isGettable方法会一定返回false, 所以ReadWriteCache 会从DB去load 这个entity 的最新状态

 

   /**
		 * locks are not returned to the client!
		 */
		public boolean isGettable(long txTimestamp) {
			return false;
		}
 

 2. 那么从db load 最新值之后, 回写cache 呢

 

ublic boolean isPuttable(long txTimestamp, Object newVersion, Comparator comparator) {
			if (timeout < txTimestamp) return true;
			if (multiplicity>0) return false;
			return version==null ?
				unlockTimestamp < txTimestamp :
				comparator.compare(version, newVersion) < 0; //by requiring <, we rely on lock timeout in the case of an unsuccessful update!
		}
 

 

如果当前cache 中的键值对是<key, Lock>, 那么我们取到的最新值会判断

  • 是否cache 中的Lock 已经比当前时间小, 如果cache 的锁定时间已过, 那么我们用<key, Item> 去替换它
  • 当前对象的版本号是否比cache 中锁的版本号大, 如果版本号大, 那么我们也用<key, Item> 去替换它

  这也是为什么我们上面在release,  afterUpdate 中把超时时间缩减的意义所在, 就是如果这个时候我不确定当前锁是否能否让<key, Item>来代替, 那么我们就把锁的超时时间缩小, 寄希望于后面的读Entity 操作从DB 中拿到最新的值之后生成Item来代替它。

最糟糕的case

问题说明

在我们的当前代码中, 我们大部分的逻辑是这样的

 

entityManager.persist(entity);
entityManager.flush();
entityManager.refresh(entity);
return entity;

 这里特别要注意的是entityManager.refresh(entity) 的目的是为了让entity 对象上的二级对象生成代理类。但是refresh也带来了非常大的副作用, 那就是在Hibernate 中注册的DefaultRefreshEventListener, 当refresh 发生的时候,onRefresh 方法会强行把key 所对应的键值对从底层cache 中擦除, 再把refresh 得来的 entity 写回到 底层cache 中。

从上面的分析我们知道, 在事务发生的时候, 我们希望在事务里面, 我们往cache 中写入的值是一个<key, lock>  而不是<key, item>这样的好处有以下好处

如果事务失败, 不管我们后续在事务外的  release()  【for entity delete case, rollback case】, afterUpdate() 【for entity update case】  因为我们在cache 中的是lock, 那么如果有entity 读的动作发生, 我们会从db 中获得最新值得并最终把它替换掉; 如果后续有entity写的动作发生, 那么我们要么会用新的锁去替换已有锁或者用在entity db 更新完成后用item 去替换它;

 否则, 如果事务失败, 而后续事务外的release()  【for entity delete case, rollback case】, afterUpdate() 【for entity update case】  调用又失败了, 那么我们在cache 中将存储了一个与DB中不一致的<key, Item>键值对。

解决方案

因为refresh的DefaultRefreshEventListener 打破了CacheConcurrencyStrategy 对底层cache 实现的封装, 它是绕过CacheConcurrencyStrategy的实现类直接调用memcached或者ehcache等底层cache 实现, 在transaction 中对底层cache 进行了写操作, 导致二级缓存使用ReadWriteCache 可能会带来的cache 不一致问题;

应对办法就是把  entityManager.refresh(entity);  替换成  entity = entityManager.find(type, entity.getId());

find 是严格执行    transaction begin -> get() --> transaction end - -> put() 操作顺序的。

 

 

其实这里说这么多都是在分析Hibernate 中ReadWriteCache的 SoftLock 在分布式环境中集中式cache 存储中的readwrite lock 的锁实现。 这是一个很巧妙的设计, 如果我们其他地方有用到是可以直接拿来稍微改造下就可以用的。

 

 

另外Hibernate 到CacheConcurrencyStrategy 的具体实现类的接口调用的时候, 具体的entity 就已经被序列化好了, 所以说hibernate 的序列化和反序列化和我们使用CacheConcurrencyStrategy 哪种cache 策略无关, 和具体底层的cache provider 实现也是无关的。

 

 

其他注意要点

在分布式的系统中, 因为App 是集群的, 但是cache 在逻辑上是中心化的。 这里有个问题, 我们所有的锁都依赖App Server 上的时间, 所以App Server之间的时钟必须是同步的, 至少误差不能大于我们这个算法所容忍的数量级。

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics