Redis分布式锁思考

1 篇文章 0 订阅

一般的锁只能针对单机下同一进程的多个线程,或单机的多个进程。多机情况下,对同一个资源访问,需要对每台机器的访问进程或线程加锁,这便是分布式锁。分布式锁可以利用多机的共享缓存(例如redis)实现。redis的命令文档[1],实现及分析参考文档[2]。

利用redis的get、setnx、getset、del四个命令可以实现基于redis的分布式锁:

  • get key:表示从redis中读取一个key的value,如果key没有对应的value,返回nil,如果存储的值不是字符串类型,返回错误
  • setnx key value:表示往redis中存储一个键值对,但只有当key不存在时才成功,返回1;否则失败,返回0,不改变key的value
  • getset key:将给定 key 的值设为 value ,并返回 key 的旧值(old value)。当旧值不存在时返回nil,当旧值不为字符串类型,返回错误
  • del key:表示删除key,当key不存在时不做操作,返回删除的key数量

关于加锁思考,循环中:
0、setnx的value是当前机器时间+预估运算时间作为锁的失效时间。这是为了防止获得锁的线程挂掉而无法释放锁而导致死锁。
0.1、返回1,证明已经获得锁,返回啦
0.2、返回0,获得锁失败,需要检查锁超时时间
1、get 获取到锁,利用失效时间判断锁是否失效。
1.1、取锁超时时间的时刻可能锁被删除释放,此时并没有拿到锁,应该重新循环加锁逻辑。
2、取锁超时时间成功
2.1、锁没有超时,休眠一下,重新循环加锁
2.2、锁超时,但此时不能直接释放锁删除。因为此时可能多个线程都读到该锁超时,如果直接删除锁,所有线程都可能删除上一个删除锁又新上的锁,会有多个线程进入临界区,产生竞争状态。
3、此时采用乐观锁的思想,用getset再次获取锁的旧超时时间。
3.1、如果此时获得锁旧超时时间成功
3.1.1、等于上一次获得的锁超时时间,证明两次操作过程中没有别人动过这个锁,此时已经获得锁
3.1.2、不等于上一次获得的锁超时时间,说明有人先动过锁,获取锁失败。虽然修改了别人的过期时间,但因为冲突的线程相差时间极短,所以修改后的过期时间并无大碍。此处依赖所有机器的时间一致。
3.2、如果此时获得锁旧超时时间失败,证明当前线程是第一个在锁失效后又加上锁的线程,所以也获得锁
4、其他情况都没有获得锁,循环setnx吧

关于解锁的思考:
在锁的时候,如果锁住了,回传超时时间,作为解锁时候的凭证,解锁时传入锁的键值和凭证。我思考的解锁时候有两种写法:
1、解锁前get一下键值的value,判断是不是和自己的凭证一样。但这样存在一些问题:

  • get时返回nil的可能,此时表示有别的线程拿到锁并用完释放
  • get返回非nil,但是不等于自身凭证。由于有getset那一步,当两个竞争线程都在这个过程中时,存在持有锁的线程凭证不等于value,而是value是稍慢那一步线程设置的value。

2、解锁前用凭证判断锁是否已经超时,如果没有超时,直接删除;如果超时,等着锁自动过期就好,免得误删别人的锁。但这种写法同样存在问题,由于线程调度的不确定性,判断到删除之间可能过去很久,并不是绝对意义上的正确解锁。

一个样例代码

public class RedisLock {

    private static final Logger logger = LoggerFactory.getLogger(RedisLock.class);

    //显然jedis还需要自己配置来初始化
    private Jedis jedis = new Jedis();

    //默认锁住15秒,尽力规避锁时间太短导致的错误释放
    private static final long DEFAULT_LOCK_TIME = 15 * 1000;

    //尝试锁住一个lock,设置尝试锁住的次数和超时时间(毫秒),默认最短15秒
    //成功时返回这把锁的key,解锁时需要凭借锁的lock和key
    //失败时返回空字符串
    public String lock(String lock, int retryCount, long timeout) {
        Preconditions.checkArgument(retryCount > 0 && timeout > 0, "retry count <= 0 or timeout <= 0 !");
        Preconditions.checkArgument(retryCount < Integer.MAX_VALUE && timeout < Long.MAX_VALUE - DEFAULT_LOCK_TIME,
                "retry count is too big or timeout is too big!");
        String $lock = Preconditions.checkNotNull(lock) + "_redis_lock";
        long $timeout = timeout + DEFAULT_LOCK_TIME;
        String ret = null;
        //重试一定次数,还是拿不到,就放弃
        try {
            long i, status;
            for (i = 0, status = 0; status == 0 && i < retryCount; ++i) {
                //尝试加锁,并设置超时时间为当前机器时间+超时时间
                if ((status = jedis.setnx($lock, ret = Long.toString(System.currentTimeMillis() + $timeout))) == 0) {
                    //获取锁失败,查看锁是否超时
                    String time = jedis.get($lock);
                    //在加锁和检查之间,锁被删除了,尝试重新加锁
                    if (time == null) {
                        continue;
                    }
                    //锁的超时时间戳小于当前时间,证明锁已经超时
                    if (Long.parseLong(time) < System.currentTimeMillis()) {
                        String oldTime = jedis.getSet($lock, Long.toString(System.currentTimeMillis() + $timeout));
                        if (oldTime == null || oldTime.equals(time)) {
                            //拿到锁了,跳出循环
                            break;
                        }
                    }
                    try {
                        TimeUnit.MILLISECONDS.sleep(1L);
                    } catch (InterruptedException e) {
                        logger.error("lock key:{} sleep failed!", lock);
                    }
                }
            }
            if (i == retryCount && status == 0) {
                logger.info("lock key:{} failed!", lock);
                return "";
            }
            //给锁加上过期时间
            jedis.pexpire($lock, $timeout);
            logger.info("lock key:{} succsee!", lock);
            return ret;
        } catch (Exception e) {
            logger.error("redis lock key:{} failed! cached exception: ", lock, e);
            return "";
        }
    }

    //释放lock的锁,需要传入lock和key
    //尽力确保删除属于自己的锁,但是不保证做得到
    public void releaseLock(String lock, String key) {
        String $lock = Preconditions.checkNotNull(lock) + "_redis_lock";
        Preconditions.checkNotNull(key);
        try {
            long timeout = Long.parseLong(key);
            //锁还没有超时,锁还属于自己可以直接删除
            //但由于线程运行的不确定性,其实不能完全保证删除时锁还属于自己
            //真正执行删除操作时,距离上语句判断可能过了很久
            if (timeout <= System.currentTimeMillis()) {
                jedis.del($lock);
                logger.info("release lock:{} with key:{} success!", lock, key);
            } else {
                logger.info("lock:{} with key:{} timeout! wait to expire", lock, key);
            }
        } catch (Exception e) {
            logger.error("redis release {}  with key:{} failed! cached exception: ", lock, key, e);
        }
    }
}

2、解锁前用凭证判断锁是否已经超时,如果没有超时,直接删除;如果超时,等着锁自动过期就好,免得误删别人的锁。但这种写法同样存在问题,由于线程调度的不确定性,判断到删除之间可能过去很久,并不是绝对意义上的正确解锁。对于新版的redis,在set方法中通过两个参数达到一条命令执行。在旧版的redis中使用pipeline的方式也能达到这个效果。

关于解锁我只想到这么多,希望有帮助,欢迎拍砖多交流。

参考链接:
[1].http://doc.redisfans.com/
[2].http://blog.csdn.net/ugg/article/details/41894947

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值