分布式锁是控制分布式系统或不同系统之间共同访
言七墨 问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
背景
由于线上出现了并发问题,导致出现了很多脏数据,分布式环境下,考虑到乐观锁无法解决,也只能请教于分布式锁了。实现分布式锁的方式有:数据库乐观锁、基于 Redis 的分布式锁、基于 Memcached 的分布式锁、基于 ZooKeeper 的分布式锁,本文只总结下“基于 Redis 的分
分布式锁的实现要点
- 互斥性:在任意时刻,只有一个客户端能持有锁
- 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
- 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了(见下文中的 Jedis)
- 具有容错性:只要大部分的 Redis 节点正常运行,客户端就可以加锁和解锁了(暂时不考虑分区容错性的问题)
注意看以下代码注释:
1、基于 redis 2.1.*及以上版本使用 setIfAbsent
# 使用支持同时设置过期时间的 setIfAbsent 方法
public Boolean getLock(String key, String value, Long expired) {
# 原子性
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, value, expired, TimeUnit.SECONDS);
if (isLock) {
return true;
}
return false;
}
2、基于 redis 1.5.* 版本使用 setIfAbsent
# 使用 redis 事务 + SessionCallBack 保证事务的原子性
public Boolean getLock(String key, String value, Long expired) {
log.info(String.format("lockKey : %s", key));
// java 使用 redis 的事务时,不能直接用 Api 中的 multi() 和 exec() ,因为 multi() 和 exec() 是在两 connect 中的,会导致死锁
// 故采用 SessionCallBack 的方式保证 multi() 和 exec() 总是共用同一个连接
SessionCallback<Boolean> sessionCallback = new SessionCallback<Boolean>() {
List<Object> exec = null;
@Override
@SuppressWarnings("unchecked")
public Boolean execute(RedisOperations operations) throws DataAccessException {
operations.multi();
redisTemplate.opsForValue().setIfAbsent(key, value);
redisTemplate.expire(key, expired, TimeUnit.SECONDS);
exec = operations.exec();
if(exec.size() > 0) {
return (Boolean) exec.get(0);
}
return false;
}
};
return redisTemplate.execute(sessionCallback);
}
3、基于 Jedis 2.9.* 版本使用 setNx
- 加锁代码(原子性):
/**
* 尝试获取分布式锁
* @param Jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @param expired 超期时间
* @return 是否获取成功
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expired) {
// 第三个参数:
// "NX":意思是 SET IF NOT EXIST,即当 key 不存在时,我们进行 set 操作;若 key 已经存在,则不做任何操作
// "XX":意思是当 key 存在时,我们才进行 set 操作(很少用)
// 第四个参数:意思是我们要给这个 key 加一个过期的设置,具体时间由第五个参数决定
// "PX":毫秒
// "EX":秒
String result = jedis.set(lockKey, requestId, "NX", "PX", expired);
if ("OK".equals(result)) {
return true;
}
return false;
}
- 解锁代码(使用 Lua 脚本保持多个操作的原子性):
/**
* 释放分布式锁
* @param Jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseLock(Jedis jedis, String lockKey, String requestId) {
// Lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// eval()方法是将Lua代码交给Redis服务端执行
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
if (1L == Long.parseLong(result.toString())) {
return true;
}
return false;
}
结尾
本次线上修复并发问题使用的是“方式2 :基于 redis 1.5.* 版本使用 setIfAbsent”,并且经过了压力测试,完美解决。针对另外两种方式,方式1 和 方式3 都能保证操作的原子性,理论上是没有问题的,故没有模拟并发场景进行测试
更加优雅的实现,可参见:基于 Redisson 实现切面分布式锁