原文:https://redis.io/topics/distlock
翻译:http://udn.yyuap.com/article-6007.html
缓存主要目的是加快查询的速度,最好不要频繁的改动。
但现在有个需求,网站每个店铺的访问量,这个数值,是频繁变动的,也放到redis中了。而且用分布式部署。
对这个数值的操作是读出来,加一,再写回去。这三步应该是原子操作,不应该多个线程交叉进行,否则会出现错误。
如果没有分布式,代码只在一个机器上运行,那么可以在这段代码上加上synchronized关键字保证线程同步。
其实synchronized也就是将一个对象作为锁,线程要想执行代码,必须先获得锁,同一时间只有一个线程有锁,就可以保证线程同步。
参考:https://www.cnblogs.com/QQParadise/articles/5059824.html(synchronized)
现在是分布式系统,一份代码部署到了几台机器上,要想实现线程同步,同样需要一把锁,这个锁可以放到redis中。
参考:https://redis.io/commands/set(set命令)
https://redis.io/commands/setnx(setnx命令)
如果只有一条redis服务器,可以这样做:
对每个店铺设一个锁
SET store_id current_time_mills NX PX 30000
如果store_id不存在,就将它的值设为一个唯一值,设置过期时间为30s,返回值为“OK”,获得锁。如果store_id已经存在,就什么都不做,返回值为“NULL”,没有获得锁,等待,再试。
如上图所示,一个线程获得锁后,分几种情况,一个是线程出错了,他没有正常结束,这样锁到期后,会自动打开;还有一种情况是线程正常执行,执行完后要释放锁,释放锁,就是将store_id关键词删除。这又分两种情况,一种是执行时间比锁过期时间短,执行完,删除锁,没有问题;还有一种情况是执行时间比较长,超过了锁过期时间,这是要是删除的话,可能就是删除别的线程的锁了,这样就会出现多个线程同时执行。所以在删除时简单判断下,就可以避免这种情况:
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end
判断值是不是当初设的值(所有这个值要唯一),如果是删除,如果不是什么都不做。
如果只有一个redis,这个redis崩溃后,同步机制就瘫痪了。多个redis保证安全。
多个redis是这样做的:
Redlock算法:
- 记录当前时间,以毫秒为单位;
- 以串行的方式尝试从所有的N个实例中获取锁,使用的是相同的key值和相同的随机的value值。在从每个redis实例获取锁时,客户端会设置一个连接超时,其时长相比锁的自动释放时间要短的多。例如,如果锁的自动释放时间是10s,那么连接超时大概设置在5~50ms之间。这可以避免当Redis节点关掉时,会长时间堵住客户端,如果这个节点没及时响应,就应该尽快转到下个节点。
- 客户端计算获取所有锁耗费的时长,方法是当前时间减去步骤1中的时间戳。当且仅当客户端能从多数节点(n/2+1)中获得锁,并且耗费时长小于锁的有效期时,可以认为锁已经获得了。
- 如果锁获得了,它的最终有效时长将重新计算为远时长将去步骤3中获取锁耗费的时长;
- 如果锁获取失败了(要么是没有锁住N/2+1个节点,要么是锁的最终有效时长为负数),客户端会对所有实例进行解锁操作(即时对没有加锁成功的实例也一样)