基于tair的分布式锁,在阿里内部技术社区已经有很多讨论了,不过基本上都是基于悲观锁的实现,基本特征是互斥或者阻塞。但实际的业务场景中,并不是所有的地方都需要悲观锁,写冲突很少的场景(例如:员工、门店信息修改),使用乐观锁更为合适。
那么如何设计和使用乐观锁呢?在看具体的代码前需要先对乐观锁的原理有个简单的了解。
乐观锁
乐观锁特点
version,版本控制,是乐观锁的根基。乐观锁并不是真的锁,它只是加了一个标识,用于区分数据是否有被意料之外的更改过。若没有,那就正常提交事物结束操作;若有,则回滚事物,撤销所有操作。所以,乐观锁除了需要应用于写冲突很少的场景,还要求系统支持事物回滚或补偿。
乐观锁操作流程
trylock(获取版本)–> transaction –> unlock(版本检查) — 正常—> version+1 commit
— 异常—> rollback
设计要求
1.只要能获取版本,就认为trylock成功
2.版本检查则更新version,一般是加1;否则,异常处理,version不变,并开启回滚
3.多事物同时获得乐观锁时,unlock只能有一个成功,其余的都应该失败
结合tair
有了上述设计要求,具体的实现就要参考tair api的特点了。这里笔者使用的是zcache,是蚂蚁基于 ldc 逻辑对 tair 的封装,接口和tair的接口略有区别,但底层的特性是相同的。
tair的存储特性要求了,内容不能长期存储;结合锁的特点,过期时间应该尽量短,这里就设计为10秒,且unlock时应该立刻删除。
tair的version控制有个不常规的特点,就是如果key不存在,第一次put进tair时,version的值一定会被重置为1.而put时输入的verison为0,则会强制更新version加1,version为1,则又可能恰好更新成功。
因此,基于tair设计乐观锁最大的难点,在于初始化版本的控制值。并且,由于trylock的作用仅仅是初始化version,所以悲观锁中的先get再put再坚持的方式并不适用。
代码实现
tryLock
private static final int INITIAL_VERSION = 10;
private static final int UPDATE_VERSION = 1;
1
2
3
4
5
6
7
8
9
10
11 1public boolean tryLock(Object key, int expire) {
2 try {
3 cacheManager.putObjectExpireResultCode(key, LOCK_NAME, INITIAL_VERSION, expire);
4 cacheManager.putObjectExpireResultCode(key, LOCK_NAME, UPDATE_VERSION, expire);
5 return true;
6 }catch (Exception e){
7 return false;
8 }
9}
10
11
tryLock后,正常情况version会被置成2,下面按照不同情景分析:
假设有多个事物Transaction A,Transaction B,位于不同进程
1.第一次put
若不存在key
因为INITIAL_VERSION=10,只有一个transaction可以put成功,使得version初始化为1
其余的transaction都会put失败,version保持1
若存在version=1的key或存在version=2的key
所有的transaction都会put失败,version保持1或2
2.第二次put
若在version=1的key或存在version=2的key
因为UPDATE_VERSION=1,只有一个transaction可以put成功,使得version初始化为2
其余的transaction都会put失败,version保持2
若存在version=2的key
所有的transaction都会put失败,version=2
3.特殊场景
若存在version=2的key
transaction A第一次put后,transaction B的unlock被执行,transaction A第二次put后version=1
若没有transaction C执行trylock,则transaction A的unlock会失败
若有transaction C执行trylock,随后version=2,则transaction A或transaction C的unlock会成功
unlock
private static final int UNLOCK_VERSION = 3;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1public void unlock(Object key) {
2 ResultCode resultCode = cacheManager.putObjectExpireResultCode(key, LOCK_NAME, LOCKED_VERSION, 0);
3 if (resultCode != null && resultCode.getCode() == ResultCode.SUCCESS.getCode()) {
4 Result<DataEntry> result = cacheManager.getObjectWithVersionInfo(key);
5 if (result != null && result.getValue() != null) {
6 if (result.getValue().getVersion() == UNLOCK_VERSION) {
7 cacheManager.removeObject(key);
8 } else {
9 throw new BizCommonException(BizCommonErrorEnum.OPTIMISTIC_LOCK_MODIFIED_ERROR);
10 }
11 } else {
12 throw new BizCommonException(BizCommonErrorEnum.ZCACHE_OPERATION_EXCEPTION);
13 }
14 } else {
15 throw new BizCommonException(BizCommonErrorEnum.ZCACHE_OPERATION_EXCEPTION);
16 }
17}
18
19
unlock时,
1.put操作
若version=2,所有的transaction都会尝试用version=2的值去put
只有第一个完成的transaction可以put成功,对应的version变为3
其他的事物则都put失败,触发回滚
若version=1,对应的transaction会失败,对应tryLock中的特殊场景
若key不存在,put成功,version变为1
2.get操作
若version=3,则删除该key,事物提交,锁释放成功
若version!=3, 则触发回滚
小结
以上个人利用tair实现乐观锁的一种解决方案,受限于tair存储时间和vertion初始值的特性,实现方案并不完美,算是抛砖引玉吧!