Redis实现分布式锁的正确姿势数据库

来源:互联网 / 作者:SKY / 2019-01-24 21:01 / 点击:
在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架——Redis。但是很多工作很多年的朋友对Redis还处于一个最基础的使用和认识。所以我就

Redis实现分布式锁的正确姿势

一、前言

在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架——Redis。但是很多工作很多年的朋友对Redis还处于一个最基础的使用和认识。所以我就像把自己对分布式缓存的一些理解和应用整理一个系列,希望可以帮助到大家加深对Redis的理解。本系列的文章思路先从Redis的应用开始。再解析Redis的内部实现原理。最后以经常会问到Redist相关的面试题为结尾。

二、分布式锁的实现要点

为了实现分布式锁,需要确保锁同时满足以下四个条件:

互斥性。在任意时刻,只有一个客户端能持有锁

不会发送死锁。即使一个客户端持有锁的期间崩溃而没有主动释放锁,也需要保证后续其他客户端能够加锁成功

加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给释放了。

容错性。只要大部分的Redis节点正常运行,客户端就可以进行加锁和解锁操作。

三、Redis实现分布式锁的错误姿势 3.1 加锁错误姿势

在讲解使用Redis实现分布式锁的正确姿势之前,我们有必要来看下错误实现方式。

首先,为了保证互斥性和不会发送死锁2个条件,所以我们在加锁操作的时候,需要使用SETNX指令来保证互斥性——只有一个客户端能够持有锁。为了保证不会发送死锁,需要给锁加一个过期时间,这样就可以保证即使持有锁的客户端期间崩溃了也不会一直不释放锁。

为了保证这2个条件,有些人错误的实现会用如下代码来实现加锁操作:

/** 

     * 实现加锁的错误姿势 

     * @param jedis 

     * @param lockKey 

     * @param requestId 

     * @param expireTime 

     */ 

    public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) { 

        Long result = jedis.setnx(lockKey, requestId); 

        if (result == 1) { 

            // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁 

            jedis.expire(lockKey, expireTime); 

        } 

    } 

可能一些初学者还没看出以上实现加锁操作的错误原因。这样我们解释下。setnx 和expire是两条Redis指令,不具备原子性,如果程序在执行完setnx之后突然崩溃,导致没有设置锁的过期时间,从而就导致死锁了。因为这个客户端持有的所有不会被其他客户端释放,持有锁的客户端又崩溃了,也不会主动释放。从而该锁永远不会释放,导致其他客户端也获得不能锁。从而其他客户端一直阻塞。所以针对该代码正确姿势应该保证setnx和expire原子性

实现加锁操作的错误姿势2。具体实现如下代码所示

/** 

     * 实现加锁的错误姿势2 

     * @param jedis 

     * @param lockKey 

     * @param expireTime 

     * @return 

     */ 

    public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) { 

        long expires = System.currentTimeMillis() + expireTime; 

        String expiresStr = String.valueOf(expires); 

        // 如果当前锁不存在,返回加锁成功 

        if (jedis.setnx(lockKey, expiresStr) == 1) { 

            return true

        } 

 

        // 如果锁存在,获取锁的过期时间 

        String currentValueStr = jedis.get(lockKey); 

        if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) { 

            // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间 

            String oldValueStr = jedis.getSet(lockKey, expiresStr); 

            if (oldValueStr != null && oldValueStr.equals(currentValueStr)) { 

                // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁 

                return true

            } 

        } 

        // 其他情况,一律返回加锁失败 

        return false

    } 

这个加锁操作咋一看没有毛病对吧。那以上这段代码的问题毛病出在哪里呢?

1. 由于客户端自己生成过期时间,所以需要强制要求分布式环境下所有客户端的时间必须同步。

阅读延展

1
3