目前业内常用的分布式锁的实现方式主要有以下几种:
- 基于Redis的分布式锁
- 基于zookeeper的分布式锁
- 基于数据库的锁,如果是更新操作可以考虑乐观锁,如果是插入操作可以考虑在某些字段上建立唯一索引(这里展开说一下,前几天和架构师聊天,如果遇到无法为单一字段建立唯一索引的业务场景,那还可以考虑联合其他字段,建立联合唯一索引,变相使用唯一索引,毕竟这是最简单的解决问题方式)
本文主要尝试使用SpringBoot集成的RedisTemplate来实现分布式锁的功能,在分布式高并发的业务场景下,我们不得不要考虑分布式锁的实现需要具备的几点要求:
- 互斥性
在任意时刻,只有一个线程能够获得锁
- 不会死锁
一个线程获得锁后,不会一直持有不释放,导致其他线程无法获得锁而影响业务
- 加解锁是同一线程
试想如果加锁的线程还没有执行完业务,被另一线程解锁,那分布式锁必定是无法解决问题的
- 健壮性
在使用集群的情况下,如果增加、删除节点,要尽量避免key的miss,如果命中率太低,势必会在某些极端情况下影响到业务,这里可以考虑一致性哈希等算法,一般由运维同学来实施;如果连接Redis时,发生jedis的超时异常,业务该如何处理,这个就要就事论事了,本文不就这点展开讨论
加锁方式
加锁的方法我们要考虑这么几个问题:
- 选择合适的变量作为key值,这个变量可以允许同一时间只有一个线程执行某段业务代码
- 相对于key的value如何定义?这里我们设定一个requestId作为value值,这个值可以在实际编程中使用UUID来生成,这样做的好处是可以保证加锁和解锁的是同一线程
- 如何防止死锁?考虑到Redis的命令可以设置生命周期,我们最好的办法就是为每个加锁的业务都要求使用者根据业务设定合理的过期时间,业务处理完之后尽可能快的释放锁
- 在高并发的场景下,同一时间只能有一个线程成功加锁,如何实现?自然我们想到Redis的setNx命令
来看一下代码:
/**
*
* @param key
* @param requestId
* @param expireTime
* @return
*/
public static boolean getLock(String key, String requestId, String expireTime) {
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/getLock.lua")));
Object result = redisTemplate.execute(redisScript,argsSerializer,resultSerializer,Collections.singletonList(key),requestId,expireTime);
if(EXEC_RESULT.equals(result)) {
return true;
}
return false;
}
我们这里引入了lua脚本,主要的好处是Redis可以通过eval命令保证代码执行的原子性,脚本内容如下:
getLock.lua:
if redis.call('setNx',KEYS[1],ARGV[1]) then
if redis.call('get',KEYS[1])==ARGV[1] then
return redis.call('expire',KEYS[1],ARGV[2])
else
return 0
end
end
解锁方式
解锁的时候我们仍然需要考虑,解锁的线程就是加锁的线程,而且解锁的操作执行命令的原子性:
/**
*
* @param key
* @param requestId
* @return
*/
public static boolean releaseLock(String key, String requestId) {
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("script/releaseLock.lua")));
Object result = redisTemplate.execute(redisScript,argsSerializer,resultSerializer,Collections.singletonList(key),requestId);
if(EXEC_RESULT.equals(result)) {
return true;
}
return false;
}
releaseLock.lua:
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
我们这里还是使用lua脚本来实现,不喜欢使用脚本的同学,可以考虑使用RedisScript的另一个方法:
redisScript.setScriptText();
最后,还要强调一下,RedisTemplate使用过程中是有些坑的,比如序列化问题以及脚本的初始化问题,执行命令返回值的类型问题,都需要通过认真的调试完成:
public class RedisUtils {
private static RedisTemplate redisTemplate;
private static DefaultRedisScript<String> redisScript;
private static RedisSerializer<String> argsSerializer;
private static RedisSerializer resultSerializer;
private static final Long EXEC_RESULT = 1L;
@PostConstruct
public void init() {
redisScript = new DefaultRedisScript<String>();
redisScript.setResultType(String.class);
argsSerializer = new StringRedisSerializer();
resultSerializer = new StringRedisSerializer();
}
@Autowired
public RedisUtils(RedisTemplate redisTemplate) {
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
this.redisTemplate = redisTemplate;
}
}
参考资料:
https://www.cnblogs.com/linjiqin/p/8003838.html
https://www.jianshu.com/p/9e7b5c5c9b7b