zoukankan      html  css  js  c++  java
  • 13. Redis实现分布式锁

    楔子

    锁是多线程编程中的一个重要概念,它是保证多线程并发时顺利执行的关键。我们通常所说的"锁"是指程序中的锁,也就是单机锁,比如Python的threading模块里面的Lock等等,而分布式锁是指可以在多机集群中使用的锁。

    锁主要用于并发控制,用于保证一项资源在任何时候只能被一个线程使用,如果其他线程也要使用同样的资源,必须排队等待上一个线程使用完。

    分布式锁

    什么是分布式锁?

    分布式锁可以简单理解为用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。

    如何实现分布式锁

    分布式锁比较常见的实现方式有三种:

    • Memcached 实现的分布式锁:使用 add 命令,添加成功的情况下,表示创建分布式锁成功。
    • ZooKeeper 实现的分布式锁:使用 ZooKeeper 顺序临时节点来实现分布式锁。
    • Redis 实现的分布式锁。

    本文要重点来说的是第三种,也就是 Redis 分布式锁的实现方式。

    Redis 分布式锁的实现思路是使用 setnx(set if not exists),如果创建成功则表明此锁创建成功,否则代表这个锁已经被占用创建失败。是的,setnx这个貌似我们在最一开始介绍字符串的操作时候就已经说过了,setnx key value,如果key不存在,value设置成功;存在则设置失败。

    分布式锁实现

    127.0.0.1:6379> setnx name hanser 
    (integer) 1  # 一开始name不存在,设置成功
    127.0.0.1:6379> setnx name yousa
    (integer) 0  # 一旦存在,则设置失败
    127.0.0.1:6379> get name
    "hanser"  # 获取name还是第一次设置的name
    127.0.0.1:6379> 
    

    当然我们是为了实现分布式锁,所以还是换个名字吧。

    127.0.0.1:6379> setnx lock true  # 设置成功
    (integer) 1
    127.0.0.1:6379> setnx lock false  # 再次设置,失败
    (integer) 0
    127.0.0.1:6379> get lock
    "true"
    127.0.0.1:6379> del lock  # 删除锁
    (integer) 1
    127.0.0.1:6379> setnx lock false  # 设置成功
    (integer) 1
    127.0.0.1:6379> get lock
    "false"
    127.0.0.1:6379> 
    

    从以上代码可以看出,释放锁使用 del lock 即可,如果在锁未被删除之前,其他程序再来执行 setnx 是不会创建成功的。

    setnx 的问题

    setnx 虽然可以成功地创建分布式锁,但存在一个问题,如果此程序在创建了锁之后,程序异常退出了,那么这个锁将永远不会被释放,就造成了死锁的问题。

    这个时候有人想到,我们可以使用 expire key seconds 设置超时时间,即使出现程序中途崩溃的情况,超过超时时间之后,这个锁也会解除,不会出现死锁的情况了,实现命令如下:

    127.0.0.1:6379> del lock
    (integer) 1
    127.0.0.1:6379> setnx lock true
    (integer) 1
    127.0.0.1:6379> expire lock 30  # 添加30s的过期时间。
    (integer) 1
    127.0.0.1:6379> 
    

    但这样依然会有问题,因为命令 setnx 和 expire 处理是一前一后非原子性的,因此如果在它们执行之间,出现断电和 Redis 异常退出的情况,因为超时时间未设置,依然会造成死锁。

    带参数的 Set

    因为 setnx 和 expire 存在原子性的问题,所以之后出现了很多类库用于解决此问题的,这样就增加了使用的成本,意味着你不但要添加 Redis 本身的客户端,并且为了解决 setnx 分布式锁的问题,还需要额外第三方类库。

    然而,这个问题到 Redis 2.6.12 时得到了解决,因为这个版本可以使用 set 并设置超时和非空判定等参数了。

    • set key value ex 30:设置key的同时指定30s的过期时间
    • set key value nx:key不存在进行设置,存在则设置失败
    • set key value xx:key存在进行设置,不存在则设置失败,没有setxx

    ex和nx、xx可以组合使用,比如set key value ex 30 nx|xx或者set key value nx|xx ex 30,顺序没有限制,这样就实现了原子操作。但是注意:nx和xx不可以一起使用。

    127.0.0.1:6379> set lock true ex 30 nx  # 设置成功
    OK
    127.0.0.1:6379> set lock false ex 30 nx  # 在锁被占用的情况下,设置失败
    (nil)
    127.0.0.1:6379> set lock false ex 30 nx  # 30s过后,锁失效,设置成功
    OK
    127.0.0.1:6379>
    

    这样我们就可以使用 set 命令来设置分布式锁,并设置超时时间了,而且 set 命令可以保证原子性。

    分布式锁的超时问题

    使用 set 命令之后好像所有问题都解决了,然后真相是"没那么简单"。使用 set 命令只解决创建锁的问题,那执行中的极端问题,和释放锁极端问题,我们依旧要考虑。

    例如,我们设置锁的最大超时时间是 30s,但业务处理使用了 35s,这就会导致原有的业务还未执行完成,锁就被释放了,新的程序和旧程序一起操作就会带来线程安全的问题。

    执行超时的问题除了带来线程安全问题之外,还引发了另一个问题:锁被误删。

    假设锁的最大超时时间是 30s,应用 1 执行了 35s,然而应用 2 在 过了30s、锁被自动释放之后,重新获取并设置了锁,然后在 35s 时,应用 1 执行完之后,就会把应用 2 创建的锁给删除掉,如下图所示:

    锁被误删的解决方案是在使用 set 命令创建锁时,给 value 值设置一个归属人标识,例如给应用关联一个 UUID,每次在删除之前先要判断 UUID 是不是属于当前的线程,如果属于在删除,这样就避免了锁被误删的问题。

    注意:如果是在代码中执行删除,不能使用先判断再删除的方法,因为判断代码和删除代码不具备原子性,因此也不能这样使用,这个时候可以使用 Lua 脚本来执行判断和删除的操作,因为多条 Lua 命令可以保证原子性。

    关于Redis中如何嵌入lua脚本,我们后面会说,所以这里就不演示了,我们在介绍Redis中嵌入lua脚本的时候会回过头来介绍这里的内容。

    说完了锁误删的解决方案,咱们回过头来看如何解决执行超时的问题,执行超时的问题可以从以下两方面来解决:

    • 1. 把执行比较耗时的任务不要放到加锁的方法内,锁内的方法尽量控制执行时长;
    • 2. 把最大超时时间可以适当的设置长一点,正常情况下锁用完之后会被手动的删除掉,因此适当的把最大超时时间设置的长一点,也是可行的。

    小结

    本文介绍了锁和分布式锁的概念,锁其实就是用来保证同一时刻只有一个程序可以去操作某一个资源,以此来保证并发时程序能正常执行的。使用 Redis 来实现分布式锁不能使用 setnx 命令,因为它可能会带来死锁的问题,因此我们可以使用 Redis 2.6.12 带来的多参数的 set 命令来申请锁,但在使用的时候也要注意锁内的业务流程执行的时间,不能大于锁设置的最大超时时间,不然会带来线程安全问题和锁误删的问题。

    除了这里的set key value ex seconds nx的方式,我们还可以通过incr的方式,比如incr lock,这样lock就为1。其它线程在获取的时候,也是用incr lock,如果这个锁被使用,那么获取的结果就是2,这样就知道这个锁已经被别的线程占用了。当lock被释放之后,再使用incr lock的时候,得到结果就是1,这个时候我们就可以使用锁了。当然它也依旧面临死锁的问题,原因我们上面已经说过了。

  • 相关阅读:
    第十四章:(2)Spring Boot 与 分布式 之 Dubbo + Zookeeper
    第十四章:(1)Spring Boot 与 分布式 之 分布式介绍
    第九章:Redis 的Java客户端Jedis
    第十三章:(2)Spring Boot 与 安全 之 SpringBoot + SpringSecurity + Thymeleaf
    第八章:(1)Redis 的复制(Master/Slave)
    java学习
    周末总结4
    java
    Cheatsheet: 2012 12.17 ~ 12.31
    Cheatsheet: 2012 10.01 ~ 10.07
  • 原文地址:https://www.cnblogs.com/traditional/p/13334147.html
Copyright © 2011-2022 走看看