中间件

基于 Redis 实现分布式锁

言七墨 · 12月7日 · 2019年 · · 2868次已读

分布式锁是控制分布式系统或不同系统之间共同访言七墨问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。

背景

由于线上出现了并发问题,导致出现了很多脏数据,分布式环境下,考虑到乐观锁无法解决,也只能请教于分布式锁了。实现分布式锁的方式有:数据库乐观锁、基于 Redis 的分布式锁、基于 Memcached 的分布式锁、基于 ZooKeeper 的分布式锁,本文只总结下“基于 Redis 的分https://qimok.cn布式锁”。搜索了很多相关文章,将所有的正确使用方式总结下,以备不时之需(本文只概括单机下的 Redis 实现分布式锁的方式。针对多机部署 https://qimok.cnRedis 的场景(基于 Redis 的分布式缓存场景),要实现分布式锁,请自行参考 Redis 官方提供的 Java 组件-Redisson(这不是本文的讨论范畴))。

分布式锁的实现要点

  1. 互斥性:在任意时刻,只有一个客户端能持有锁
  2. 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
  3. 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了(见下文中的 Jedis)
  4. 具有容错性:只要大部分的 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

https://qimok.cn
# 使用 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 实现切面分布式锁

0 条回应