前言
实现分布式锁的三种方式,1:数据库乐观锁,2:redis锁,3:zk锁,本文将说下这三种的分布式锁,因为我平时用的最多的就是redis锁,所以先以它开写。
高可用
要保证分布式锁的高可用性,需要满足以下几个条件:
1:唯一性(互斥性):同一时间,只有个客户端获得锁
2:无死锁:要保证由于解锁失败导致其他客户端一直不能获得锁。
3:容错性:要保证大部分redis节点能用就可以加锁,解锁。
4:解铃还须系铃人:要保证解锁的客户端一定是加锁的客户端。
Redis 分布式锁
问题1: 死锁
死锁1:
Long lock=jedis.setnx(key,value);
if(lock==1){
doSomeing();
jedis.del(key);
}
该demo 是存在发送比较大概率的死锁问题,如doSomeing() 发生异常就会导致 jedis.del(key)执行不了,所以死锁发生。那这时我们可能会想到给doSomeing()一个 try cath finally。
死锁2:
Long lock=jedis.setnx(key,value);
if(lock==1){
try{
doSomeing();
}catch(Exception e){
LOGGER.error(".....");
}finally{
jedis.del(key);
}
}
这个demo 看上去挺nice的,finally 一定会执行(如果排除,kill 9情况,和doSomeing中有System.exit(0) ) ,但是该demo 还是存在一定概率发生死锁问题。如果jedis.del(key) 出现连接数据库异常,就会发生死锁。那这时我们可能想到,redis中还可以设置过期时间,那我们只要给锁加上一个过期时间是不是就ok了呢?
问题2:错误解锁
错误解锁1:
Long lock=jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if(lock==1){
try{
doSomeing();
}catch(Exception e){
LOGGER.error("...");
}finally{
jedis.del(key);
}
}
该demo 我们已经加上了过期时间,当然过期时间的设置是有要求的–要远大于业务执行时间,这里就对业务执行时间进行预估,比如业务正常最大执行时间为5s,那么我们设置成1m。这样加上过期时间,我们就可以解决之前jedis.del(key) 连接数据库失败的问题,如果失败,到了设置的过期时间还是可以释放的,解决了死锁问题。
但是这样就没有问题了吗?
举个例子:
假设我们在doSomeing() 发生了阻塞执行了2m,在这过程锁已经释放,其他客户端已经获得该锁,然后doSomeing() 执行完,执行了jedis.del(key) 这时就会出现问题,既违反了解铃还须系铃人。
错误解锁2:
UUID uuid=UUID.randomUUID();
Long lock=jedis.set(key, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if(lock==1){
try{
doSomeing();
}catch(Exception e){
LOGGER.error("...");
}finally{
if(uuid.equals(jedis.get(key))){
jedis.del(key);
}
}
}
该demo 我们给value 设置了一个uuid,保证锁能被标识为那个客户端。一个客户端只能删除自己的加的锁。这样我们再看上面的问题,好像也没有太大问题,就算doSomeing 阻塞了2m 后,继续执行也不会删除锁,因为此时key对应的value已经改变。
但是 仔细想想是不是还有问题呢? 是的,由于判断和删除加在一起不是原子的,导致了如果在判断 uuid.equals(jedis.get(key)) 为true,但是 在执行jedis.del(key)前被其他改了,这时候还是会导致错误解锁。
所以我们只要保证判断和删除时原子的就完美解决了。
那么我们如果保证它是原子的呢? 如果仅仅靠jedis 是不行的,它没有提供类似的操作。我们可以利用lua脚本来保证,redis中执行lua 脚本是原子的。
正确版:
UUID uuid=UUID.randomUUID();
Long lock=jedis.set(key, uuid, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if(lock==1){
try{
doSomeing();
}catch(Exception e){
LOGGER.error("...");
}finally{
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(uuid));
}
}
这样已经解决了前面的所有问题,但是每次要写这样一坨东西,是不是挺烦,我们可以给他封装下,具体封装就不写了。
数据库乐观锁
乐观锁: 假设大部分情况不会发生冲突,所以只有在提交数据时检测是否冲突。
version
乐观锁一种实现就是通过version 来实现:
举个例子:我们要更新货品数量
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL DEFAULT '',
`remaining_number` int(11) NOT NULL,
`version` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
思路:我们先查询 某货物信息,获得version,更新时带上version 查询。
<update id="updateGoodCAS" parameterType="com.ztl.domain.Goods">
<![CDATA[
update goods
set `name`=#{name},
remaining_number=#{remainingNumber},
version=version+1
where id=#{id} and version=#{version}
]]>
</update>
这样就不会存在同时更新某个货物库存问题,导致库存错误。