分布式锁

由此可见分布式锁的目的其实很简单,就是为了保证多台服务器在执行某一段代码时保证只有一台服务器执行

可靠性

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

  1. 互斥性。在任意时刻,只有一个客户端能持有锁。
  2. 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
  3. 具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人。保证上锁和解锁都是同一个客户端,客户端自己不能把别人加的锁给解了。

一般来说,实现分布式锁的方式有以下三种:

  • 使用数据库(MySQL),基于唯一索引。
  • 使用ZooKeeper,基于临时有序节点。
  • 使用Redis,基于setnx命令。

redis部署模式:

一、单机模式

二、主从模式

三、哨兵模式

四、集群模式

详细讲解:


redis分布式锁

  简单redis分布式锁实现。上代码,使用redisTemplate

public class RedisLock {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 加锁,自旋重试三次。锁不可重入。
     * @param lockKey
     * @param requestId
     * @param expireTime (秒)
     * @return
     */
    public boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
        boolean locked = false;
        int tryCount = 3;
        while (!locked && tryCount > 0) {
            // SET lockKey requestId NX PX 30000
            locked = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.SECONDS);

            if (!locked) {
                tryCount--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    log.error("线程被中断" + Thread.currentThread().getId(), e);
                }
            } else {
                //获取锁成功,后续逻辑(watchdog etc.)
            }
        }
        return locked;
    }

    /**
     * 非原子解锁,可能解别人锁,不安全
     * @param lockKey
     * @param requestId
     * @return
     */
    public boolean unlock(String lockKey, String requestId) {
        if (lockKey == null || requestId == null)
            return false;
        boolean releaseLock = false;
        String currentId = (String) redisTemplate.opsForValue().get(lockKey);
        if (requestId.equals(currentId)) {
            releaseLock = redisTemplate.delete(lockKey);
        }
        return releaseLock;
    }

    private final String Release_Script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * 使用lua脚本解锁,不会解除别人锁
     * @param lockKey
     * @param requestId
     * @return
     */
    public boolean releaseDistributedLock(String lockKey, String requestId) {
        if (lockKey == null || requestId == null)
            return false;
        // 指定 lua 脚本,并且指定返回值类型
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
        // *!* [如果要返回值,必须设置返回映射对象],不然返回会全部是null。
        redisScript.setResultType(Long.class);
        redisScript.setScriptSource(new StaticScriptSource(Release_Script));

        Object result = redisTemplate.execute(redisScript,
                Collections.singletonList(lockKey), requestId);
        return result.equals(1L);
    }

}

加锁:

 SET lockKey requestId NX PX 30000    requestId ,客户端唯一ID;NX,意思是SET IF NOT EXIST;PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。

我们的加锁代码满足我们可靠性里描述的三个条件。

解锁:

通过lua脚本,实现原子操作:先获取当前锁的requestId是否等于传入的requestId,相同则删除当前锁。

问题点

1.锁只能加一次,不可重入。

在Redisson实现可重入锁的思路,使用Redis的哈希表存储可重入次数,当加锁成功后,使用hset命令,value(重入次数)则是1。如果同一个客户端再次加锁成功,则使用hincrby自增加一。解锁时,先判断可重复次数是否大于0,大于0则减一,否则删除键值,释放锁资源。

2.业务操作比锁有效时间长,业务代码还没执行完就自动给我解锁了

给锁续期:使用watchDog机制实现锁的续期。当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。

3.加锁失败后阻塞等待,等锁释放后再次尝试加锁。

需要利用发布订阅的机制进行优化。

4.一旦发生redis master宕机,主备切换,redis slave变为了redis master。可能会导致多个客户端对一个分布式锁完成了加锁。

所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。

为此,Redis 的作者提出一种解决方案,就是我们经常听到的 Redlock(红锁)。转:所以,我对 Redlock 的个人看法是,尽量不用它,而且它的性能不如单机版 Redis,部署成本也高,我还是会优先考虑使用 Redis「主从+哨兵」的模式,实现分布式锁。

Redisson  框架

redisson,提供了基于netty,redis的一系列封装:分布式对象、分布式集合队列、分布式锁等分布式服务组件。

如果你的项目中Redis是多机部署的,那么可以尝试使用Redisson实现分布式锁,这是Redis官方提供的Java组件。


zookeeper or redis

没有绝对的好坏,只有更适合自己的业务。就性能而言,redis很明显优于zookeeper;就分布式锁实现的健壮性而言,zookeeper很明显优于redis。如何选择,取决于你的业务!