Redis实现分布式锁的最优方案

日常开发中我们少不了要用分布式锁,保证资源访问的互斥,下面将一步步给出最佳的解决方案:

1、redis2.6以上可以用lua脚本实现加锁和设置有效期的原子性操作,但是redis2.6以下不支持lua脚本,只能采取get+setex或setnx+expire两种方案,前者忽略掉了get和setex之间的并发,而后者是忽视了ex失败的问题。

2、可以升级到新版本,支持set同时设置nx和ex参数,实现如下(加锁失败的自旋没有实现,可根据业务自行实现):

<?php
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    $key = 'key:1';
    $random = random(); //自定义的随机值函数
    $ok = $redis->set($key, 1, ['nx', 'ex'=>$expire])
    if( $ok ){
        doSomethings(); //执行业务
        if( $redis->get($key) == $random ) {
            //用随机值,防止当前线程执行时间过长,有效期到了之后自动删除当前锁,其他线程获得锁并加锁,当前线程超时结束后误删其他线程对当前资源加的锁
            del($key);
        }
    }

?>

3、上面的锁还有问题,假如在获取对比随机值和删除之间宕机了,就只能等待锁自动失效。并且在高并发场景下,应该尽量减少redis命令的次数,所以在释放锁时,我们可以用lua实现原子命令,改动后的代码如下:

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$key = 'key:1';
$random = uniqid(); //假如并发极高,同微秒还是会生成相同的id,可以通过参数附加随机前缀,也可以参考我的另一篇文章用雪花算法生成uuid
$expire = 10;

$ok = $redis->set($key, $random, ['nx', 'ex' => $expire]);
if ($ok) {
    sleep(5); //模拟执行业务

    $script = '
            if redis.call("GET", KEYS[1]) == ARGV[1] then
                return redis.call("DEL", KEYS[1])
            else
                return 0
            end
        ';
    $redis->eval($script, [$key, $random], 1);
}
127.0.0.1:6379> monitor
1594377461.656859 [0 127.0.0.1:58550] "SET" "key:1" "5f0844f5a0357" "ex" "10" "nx"
1594377466.663522 [0 127.0.0.1:58550] "EVAL" "\n            if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n                return redis.call(\"DEL\", KEYS[1])\n            else\n                return 0\n            end\n        " "1" "key:1" "5f0844f5a0357"
1594377466.663617 [0 lua] "GET" "key:1"
1594377466.663646 [0 lua] "DEL" "key:1"

4、上面的代码你以为就万无一失了?假如我们的redis服务器宕机了怎么办,这时有人可能会脱口而出,主从复制+哨兵啊,仔细想想此时会不会出现新问题?

肯定是会的,A线程在master加锁后,假如在锁的有效期內,主节点宕机,从节点升级为主节点,此时B线程又来获取锁,这时新的主节点是没有锁的,就会导致B线程加锁成功并获取资源,这就出现了锁竞态。那么这个问题如何解决呢?redis的开发者已经给我们提供了现成的解决方法:redlock。

核心思想:使用多个redis主节点(要大于等于3个),加锁时所有节点都加锁,只有半数以上的节点加锁成功才算成功,释放锁时所有节点都释放。

redlock代码:https://github.com/ronnylt/redlock-php/blob/master/src/RedLock.php

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AirGo.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值