手机网站建设新闻,手机优化系统,广州短视频seo哪家好,新建网站外链怎么做前言
我们的系统都是分布式部署的#xff0c;日常开发中#xff0c;秒杀下单、抢购商品等等业务场景#xff0c;为了防⽌库存超卖#xff0c;都需要用到分布式锁。
分布式锁其实就是#xff0c;控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或…前言
我们的系统都是分布式部署的日常开发中秒杀下单、抢购商品等等业务场景为了防⽌库存超卖都需要用到分布式锁。
分布式锁其实就是控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源往往需要互斥来防止彼此干扰以保证一致性。
业界流行的分布式锁实现一般有这3种方式
基于数据库实现的分布式锁。基于Redis实现的分布式锁。基于Zookeeper实现的分布式锁。 那下面我们就来聊聊基于数据库实现的分布式锁。
基于Redis实现的分布式锁
Redis分布式锁一般有以下这几种实现方式
setnx expire。setnx value值是过期时间。set的扩展命令(set ex px nx)。set ex px nx 校验唯一随机值,再删除。Redisson。Redisson RedLock。
setnx expire
聊到Redis分布式锁很多小伙伴反手就是setnx expire如下
ifjedis.setnx(key,lock_value) 1{ //setnx加锁expirekey100; //设置过期时间try {do something //业务处理}catch(){}finally {jedis.del(key); //释放锁}
}这段代码是可以加锁成功但是你有没有发现问题加锁操作和设置超时时间是分开的。假设在执行完setnx加锁后正要执行expire设置过期时间时进程crash掉或者要重启维护了那这个锁就长生不老了别的线程永远获取不到锁啦所以分布式锁不能这么实现!
long expires System.currentTimeMillis() expireTime; //系统时间设置的过期时间
String expiresStr String.valueOf(expires);// 如果当前锁不存在返回加锁成功
if (jedis.setnx(key, expiresStr) 1) {return true;
}
// 如果锁已经存在获取锁的过期时间
String currentValueStr jedis.get(key);// 如果获取到的过期时间小于系统当前时间表示已经过期
if (currentValueStr ! null Long.parseLong(currentValueStr) System.currentTimeMillis()) {// 锁已过期获取上一个锁的过期时间并设置现在锁的过期时间不了解redis的getSet命令的小伙伴可以去官网看下哈String oldValueStr jedis.getSet(key, expiresStr);if (oldValueStr ! null oldValueStr.equals(currentValueStr)) {// 考虑多线程并发的情况只有一个线程的设置值和当前值相同它才可以加锁return true;}
}//其他情况均返回加锁失败
return false;
}日常开发中有些小伙伴就是这么实现分布式锁的但是会有这些缺点
过期时间是客户端自己生成的分布式环境下每个客户端的时间必须同步。没有保存持有者的唯一标识可能被别的客户端释放/解锁。锁过期的时候并发多个客户端同时请求过来都执行了jedis.getSet()最终只能有一个客户端加锁* 成功但是该客户端锁的过期时间可能被别的客户端覆盖。
set的扩展命令(set ex px nx)
这个命令的几个参数分别表示什么意思呢?跟大家复习一下
SET key value [EX seconds] [PX milliseconds] [NX|XX]EX second 设置键的过期时间为second秒。 PX millisecond 设置键的过期时间为millisecond毫秒。 NX 只在键不存在时才对键进行设置操作。 XX 只在键已经存在时才对键进行设置操作。 ifjedis.set(key, lock_value, NX, EX, 100s) 1{ //加锁try {do something //业务处理}catch(){}finally {jedis.del(key); //释放锁}
}这个方案可能存在这样的问题
锁过期释放了业务还没执行完。锁被别的线程误删。
有些伙伴可能会有个疑问就是锁为什么会被别的线程误删呢?假设并发多线程场景下线程A获得了锁但是它没释放锁的话线程B是获取不到锁的所以按道理它是执行不到加锁下面的代码滴怎么会导致锁被别的线程误删呢?
假设线程A和B都想用key加锁最后A抢到锁加锁成功但是由于执行业务逻辑的耗时很长超过了设置的超时时间100s。这时候Redis就自动释放了key锁。这时候线程B就可以加锁成功了接下啦它也执行业务逻辑处理。假设碰巧这时候A执行完自己的业务逻辑它就去释放锁但是它就把B的锁给释放了。
set ex px nx 校验唯一随机值,再删除
为了解决锁被别的线程误删问题。可以在set ex px nx的基础上加上个校验的唯一随机值如下
jedis.set(key, uni_request_id, NX, EX, 100s) 1{ //加锁try {do something //业务处理}catch(){}finally {//判断是不是当前线程加的锁,是才释放if (uni_request_id.equals(jedis.get(key))) {jedis.del(key); //释放锁}}
}在这里判断当前线程加的锁和释放锁不是一个原子操作。如果调用jedis.del()释放锁的时候可能这把锁已经不属于当前客户端会解除他人加的锁。 一般可以用lua脚本来包一下。lua脚本如下
if redis.call(get,KEYS[1]) ARGV[1] then return redis.call(del,KEYS[1])
elsereturn 0
end;这种方式比较不错了一般情况下已经可以使用这种实现方式。但是还是存在锁过期释放了业务还没执行完的问题。
Redisson
对于可能存在锁过期释放业务没执行完的问题。我们可以稍微把锁过期时间设置长一些大于正常业务处理时间就好啦。如果你觉得不是很稳还可以给获得锁的线程开启一个定时守护线程每隔一段时间检查锁是否还存在存在则对锁的过期时间延长防止锁过期提前释放。
当前开源框架Redisson解决了这个问题。可以看下Redisson底层原理图 只要线程一加锁成功就会启动一个watch dog看门狗它是一个后台线程会每隔10秒检查一下如果线程1还持有锁那么就会不断的延长锁key的生存时间。因此Redisson就是使用watch dog解决了锁过期释放业务没执行完问题。
Redisson RedLock
前面六种方案都只是基于Redis单机版的分布式锁讨论还不是很完美。因为Redis一般都是集群部署的 如果线程一在Redis的master节点上拿到了锁但是加锁的key还没同步到slave节点。恰好这时master节点发生故障一个slave节点就会升级为master节点。线程二就可以顺理成章获取同个key的锁啦但线程一也已经拿到锁了锁的安全性就没了。
为了解决这个问题Redis作者antirez提出一种高级的分布式锁算法Redlock。它的核心思想是这样的
部署多个Redis master以保证它们不会同时宕掉。并且这些master节点是完全相互独立的相互之间不存在数据同步。同时需要确保在这多个master实例上是与在Redis单实例使用相同方法来获取和释放锁。
我们假设当前有5个Redis master节点在5台服务器上面运行这些Redis实例。 RedLock的实现步骤:
获取当前时间以毫秒为单位。按顺序向5个master节点请求加锁。客户端设置网络连接和响应超时时间并且超时时间要小于锁的失效时间。(假设锁自动失效时间为10秒则超时时间一般在5-50毫秒之间,我们就假设超时时间是50ms吧)。如果超时跳过该master节点尽快去尝试下一个master节点。客户端使用当前时间减去开始获取锁时间(即步骤1记录的时间)得到获取锁使用的时间。当且仅当超过一半(N/21这里是5/213个节点)的Redis master节点都获得锁并且使用的时间小于锁失效时间时锁才算获取成功。(如上图10s 30ms40ms50ms4m0s50ms)。如果取到了锁key的真正有效时间就变啦需要减去获取锁所使用的时间。如果获取锁失败(没有在至少N/21个master实例取到锁有或者获取锁时间已经超过了有效时间)客户端要在所有的master节点上解锁(即便有些master节点根本就没有加锁成功也需要解锁以防止有些漏网之鱼)。
简化下步骤就是
按顺序向5个master节点请求加锁。根据设置的超时时间来判断是不是要跳过该master节点。如果大于等于3个节点加锁成功并且使用的时间小于锁的有效期即可认定加锁成功啦。如果获取锁失败解锁!
Redisson实现了redLock版本的锁有兴趣的小伙伴可以去了解一下哈~