赞
踩
写业务过程中,有时候会使用到锁的概念,同时只能有一个人可以操作某个行为。这个时候我们就要用到锁。
锁的方式有好几种,某些语言如php不能在内存中用锁,不能使用zookeeper加锁,使用数据库做锁又消耗比较大,这个时候我们一般会选用redis做锁机制。
本部分引用自:https://blog.csdn.net/oqkdws/article/details/81985699
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现,如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往需要互斥来防止彼此干扰来保证一致性。
比如redis的操作虽然是原子性的,但是,当你的接口被多个用户同时请求的时候,由于读取写入之间存在间隔 就会出现问题
分布式锁的主要实现有哪些?
由于读写问题 redis的锁需要读写主库 别分库了
$rs = $redis->setNX($key, $value);
if ($rs) {
//处理更新缓存逻辑
// ......
//删除锁 $redis->del($key);
}
通过 setNX 获取锁,如果成功了则更新缓存然后删除锁。其实这里有一个严重的问题:如果更新缓存的时候因为某些原因意外退出了,那么这个锁就不会被删除而一直存在,以至于缓存再也得不到更新。为了解决这个问题有人可能会想到给锁设置一个过期时间,如下
$redis->multi();
$redis->setNX($key, $value);
$redis->expire($key, $ttl);
$redis->exec(); ?>
因为 setNX 不具备设置过期时间的功能,所以要借助 Expire 来设置,同时需要使用 Multi/Exec 来确保请求的原子性,以免 setNX 成功了 Expire 却失败了。(问题就在于setnx和expire中间如果遇到crash等行为,可能这个lock就不会被释放了。于是进一步的优化方案也可在lock中存储timestamp。判断timestamp的长短。)
这样还有问题:当多个请求到达时,虽然只有一个请求的 setNX 可以成功,但是任何一个请求的 Expire 却都可以成功,这就意味着即便获取不到锁也可以刷新过期时间,导致锁一直有效,还是解决不了上面的问题。显然 setNX 满足不了需求。
Redis从 2.6.12 起,SET 涵盖了 SETEX 的功能, SET 本身又包含了设置过期时间的功能,所以使用 SET 就可以解决上面遇到的问题。
$rs = $redis->set($key, $value, array('nx', 'ex' => $ttl));
if ($rs) {
//处理更新缓存逻辑
// ......
//删除锁
$redis->del($key);
}
到这一步其实还是有问题的,如果一个请求更新缓存的时间比锁的有效期还要长,导致在缓存更新过程中锁就失效了,此时另一个请求就会获取到锁,但前一个请求在缓存更新完毕的时候,直接删除锁的话就会出现误删其它请求创建的锁的情况。
(因为有可能导致误删别人的锁的情况。比如,这个锁我上了10s,但是我处理的时间比10s更长,到了10s,这个锁自动过期了,被别人取走了,并且对它重新上锁了。那么这个时候,我再调用Redis::del就是删除别人建立的锁了。)
所以要避免这种问题,可以在创建锁的时候需要引入一个随机值并在删除锁的时候加以判断(当然随机数字可能恰巧·· 后面可改用时间戳)
$rs = $redis->set($key, $random, array('nx', 'ex' => $ttl));
if ($rs) {
//处理更新逻辑
// ......
//先判断随机数,是同一个则删除锁
if ($redis->get($key) == $random) {
$redis->del($key);
}
}
但是这里似乎还存在一个问题?比如插入操作 第一个插入10秒可能还没操作完,结果就到期了。这时候第二个请求获得了锁 进来了。结果重复插入。所以 这个时间应该设置的比操作长 保证能xxx
这个解锁也可用lua脚本实现(似乎redis官方更推荐)
官方对解锁的命令也有建议,建议使用lua脚本,先进行get,再进行del
程序变成:
$token = rand(1, 100000);//可替换为 UUID function lock() { return Redis::set("my:lock", $token, "nx", "ex", 10); } function unlock() { $script = ` if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end ` return Redis::eval($script, "my:lock", $token) } //业务代码 if (lock()) { // do something unlock(); }
//注 php如何运行lua脚本
$redis = new Redis();
// 编写lua 脚本
$lua = <<<SCRIPT
//lua脚本内容
SCRIPT;
$s = $redis->eval($lua, array('name'),1);
//后续可考虑封装为函数 更方便
$redis = RedisService::getInstance(xxxx);//写库
$random = rand(0,99999);
$key = 'lock:bind:'.$m2;
$expire_time = 20;
$can_update = $redis->set($key, $random, array('nx', 'ex' =>$expire_time))
if ($can_update) {
//处理更新逻辑
//....
//先判断随机数,是同一个则删除锁
if ($redis->get($key) == $random) {
$redis->del($key);
}
watch就是乐观锁,也就是用时间戳的方式实现,不是真正的形成等待,而是时间戳不一致时(也就是有其他线程/进程/机器修改时),令自己的修改失败。setnx方式就是类似于悲观锁了,也就是拿不到就失败,不会等到修改的时候再失败(或者说,必须拿到锁才修改)。
Redis Watch 命令用于监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断
这个以后再更新
//ps 这里里面内容还没看 也不知道好不好 我们已经知道在一个事务中只有当所有命令都依次执行完后才能得到每个结果的返回值,可是有些情况下需要先获得一条命令的返回值,然后再根据这个值执行下一条命令。例如,介绍INCR命令时曾经说过使用GET和SET命令自己实现incr函数会出现竞态条件,伪代码如下: def incr($key) $value = GET $key if not $value $value = 0 $value = $value + 1 SET $key, $value return $value 肯定会有很多读者想到可以用事务来实现incr函数以防止竞态条件,可是因为事务中的每个命令的执行结果都是最后一起返回的,所以无法将前一条命令的结果作为下一条命令的参数,即在执行SET命令时无法获得GET命令的返回值,也就无法做到增1的功能了。 为了解决这个问题,我们需要换一种思路。即在 GET 获得键值后保证该键值不被其他客户端修改,直到函数执行完成后才允许其他客户端修改该键键值,这样也可以防止竞态条件。要实现这一思路需要请出事务家族的另一位成员:WATCH。WATCH命令可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行。监控一直持续到EXEC命令(事务中的命令是在EXEC之后才执行的,所以在MULTI命令后可以修改WATCH监控的键值),如: redis> SET key 1 OK redis> WATCH key OK redis> SET key 2 OK redis> MULTI OK redis> SET key 3 QUEUED redis> EXEC (nil) redis> GET key "2" 上例中在执行WATCH命令后、事务执行前修改了key的值(即SET key 2),所以最后事务中的命令SET key 3没有执行,EXEC命令返回空结果。 学会了WATCH命令就可以通过事务自己实现incr函数了,伪代码如下: def incr($key) WATCH $key $value = GET $key if not $value $value = 0 $value = $value + 1 MULTI SET $key, $value result = EXEC return result[0] 因为EXEC命令返回值是多行字符串类型,所以代码中使用result[0]来获得其中第一个结果。 提示 由于WATCH命令的作用只是当被监控的键值被修改后阻止之后一个事务的执行,而不能保证其他客户端不修改这一键值,所以我们需要在EXEC执行失败后重新执行整个函数。 执行EXEC命令后会取消对所有键的监控,如果不想执行事务中的命令也可以使用UNWATCH命令来取消监控。比如,我们要实现hsetxx函数,作用与HSETNX命令类似,只不过是仅当字段存在时才赋值。为了避免竞态条件我们使用事务来完成这一功能: def hsetxx($key, $field, $value) WATCH $key $isFieldExists = HEXISTS $key, $field if $isFieldExists is 1 MULTI HSET $key, $field, $value EXEC else UNWATCH return $isFieldExists 在代码中会判断要赋值的字段是否存在,如果字段不存在的话就不执行事务中的命令,但需要使用UNWATCH命令来保证下一个事务的执行不会受到影响。
在Redis的分布式环境中,Redis 的作者提供了RedLock 的算法来实现一个分布式锁
加锁
解锁
向所有的Redis实例发送释放锁命令即可,不用关心之前有没有从Redis实例成功获取到锁
总结
Java环境有 Redisson 可用于生产环境,但是分布式锁还是Zookeeper会比较好
redis官方推荐的分布式锁解决方案:https://blog.csdn.net/xiaohuihuiaz/article/details/124389898
本文前面讲的是高并发场景,而非主从同步场景。
主从同步相对好解决一些,redis的setex、get即可。
这里的主从同步是mysql的
比如,当用户绑定某台终端的时候,主库成功绑定了,但是用户刷新终端列表的时候,由于高峰期主从同步延迟问题,看不到这台终端。
用户没看到效果,又去扫码,造成重复绑定某台终端。
public function insertToDb($wxid,$m2){
$redis = RedisService::getInstance(Constants::REDIS_WRITER, 'WX_MINIPROGRAM_RDS', 0, true);
$key_db_defer = 'binddefer:' . $wxid . $m2;//
if ($redis->get($key_db_defer) == 1) throw new Exception("已绑定,数据同步中,请稍后刷新查看");
//插入逻辑
//....
if(插入成功) {
$redis->setex($key_db_defer, 360, 1);
}
}
场景上
具体操作上:
比如绑定逻辑。解绑的时候要宽容一点 不要设置del或者update的limit为1 可以多一点 或者不限制。因为可能读写冲突、主从同步问题造成insert了好几条的情况。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。