在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
锁和分布式锁锁是什么?锁是一种可以封锁资源的东西。这种资源通常是共享的,通常会发生使用竞争的。 为什么需要锁?需要保护共享资源正常使用,不出乱子。 如果这个时候厕所门前有个锁,每个人都没法随便进入,而是需要先得到锁,才能进去。而得到这个锁,就需要里边的人先出来。这样就可以保证同一时刻,只有一个人在使用厕所,这个人在上厕所的期间不会有不安全的事情发生,不会中途被人闯进来了。 Java中的锁在 java 编码的时候,为了保护共享资源,使得多线程环境下,不会出现“不好的结果”。我们可以使用锁来进行线程同步。于是我们可以根据具体的情况使用synchronized 关键字来修饰一个方法,或者一段代码。这个方法或者代码就像是前文中提到的“受保护的厕所,加锁的厕所”。也可以使用 java 5以后的 Lock 来实现,与 synchronized 关键字相比,Lock 的使用更灵活,可以有加锁超时时间、公平性等优势。 分布式锁上面我们所说的 synchronized 关键字也好,Lock 也好。其实他们的作用范围是啥,就是当前的应用啊。你的代码在这个 jar 包或者这个 war 包里边,被部署在 A 机器上。那么实际上我们写的 synchronized 关键字,就是在当前的机器的 JVM在执行代码的时候发生作用的。假设这个代码被部署到了三台机器上 A,B,C。那么 A 机器中的部署的代码中的synchronized 关键字并不能控制 B,C 中的内容。 假如我们需要在 A,B,C 三台机器上运行某段程序的时候,实现“原子操作”,synchronized 关键字或者 Lock 是不能满足的。很显然,这个时候我们需要的锁,是需要协同这三个节点的,于是,分布式锁就需要上场了,他就像是在A,B,C的外面加了一个层,通过它来实现锁的控制。 redis 如何实现加锁在redis中,有一条命令,可以实现类似 “锁” 的语法是这样的: SETNX key value 他的作用是,将 使用 redis 来实现锁的逻辑就是这样的 线程 1 获取锁 -- > setnx mylock lockvalue -- > 1 获取锁成功 线程 2 获取锁 -- > setnx mylock lockvalue -- > 0 获取锁失败 (继续等待,或者其他逻辑) 线程 1 释放锁 -- > 线程 2 获取锁 -- > setnx mylock lockvalue -- > 1 获取成功 锁超时在这个例子中,我们梳理了使用 redis setnx 命令 来实现锁的逻辑。这里还需要考虑的是,锁超时的问题 ,因为当线程 1 获取了锁之后,如果业务逻辑执行很长很长时间,那么其他线程只能死等,这可不行。所以需要加上超时,结合这些考虑的情况,实际的 Java 代码可以这样写: public static boolean lock(String key,String lockValue,int expire){ if(null == key){ return false; } try { Jedis jedis = getJedisPool().getResource(); String res = jedis.set(key,lockValue,"NX","EX",expire); jedis.close(); return res!=null && res.equals("OK"); } catch (Exception e) { return false; } } retry这里执行加锁,不一定能成功。当别人正在持有锁的时候,加锁的线程需要继续尝试。这个“继续尝试”通常是“忙等待”,实现代码如下: /** * 获取一个分布式锁 , 超时则返回失败 * @param key 锁的key * @param lockValue 锁的value * @param timeout 获取锁的等待时间,单位为 秒 * @return 获锁成功 - true | 获锁失败 - false */ public static boolean tryLock(String key,String lockValue,int timeout,int expire){ final long start = System.currentTimeMillis(); if(timeout > expiredNx) { timeout = expiredNx; } final long end = start + timeout * 1000; boolean res = false; // 默认返回失败 while(!(res = lock(key,lockValue,expire))){ // 调用了上面的 lock方法 if(System.currentTimeMillis() > end) { break; } } return res; }
redis 如何释放锁根据上面所述,我们在加锁的时候执行了: 不该释放的锁但是,直接执行
因此,我们需要安全的释放锁——“不是我的锁,我不能瞎释放”。所以,我们在加锁的时候,就需要标记“这是我的锁”,在释放的时候在判断 “ 这是不是我的锁?”。这里就需要在释放锁的时候加上逻辑判断,合理的逻辑应该是这样的: 1. 线程1 准备释放锁 , 锁的key 为 mylock 锁的 value 为 thread1_magic_num 2. 查询当前锁 current_value = get mylock 3. 判断 if current_value == thread1_magic_num -- > 是 我(线程1)的锁 else -- >不是 我(线程1)的锁 4. 是我的锁就释放,否则不能释放(而是执行自己的其他逻辑)。
为了实现上面这个逻辑,我们是无法通过 redis 自带的命令直接完成的。如果,再写复杂的代码去控制释放锁,则会让整体代码太过于复杂了。所以,我们引入了lua脚本。结合Lua 脚本实现释放锁的功能,更简单,redis 执行lua脚本也是原子的,所以更合适,让合适的人干合适的事,岂不更好。 通过Lua脚本实现锁释放Lua是啥,Lua是一种功能强大,高效,轻量级,可嵌入的脚本语言。其官方的描述是: Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description. Lua 调用 redis 非常简单,并且 Lua 脚本语法也易学,对于有别的编程语言基础的程序员来说,在不学习Lua脚本语法的情况下,直接看 Lua 的代码 也是可以看懂的。例子如下: if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end 上面的代码,逻辑很简单,if 中的比较如果是true , 那么 执行 del 并返回del结果;如果 if 结果为false 直接返回 0 。这不就满足了我们释放锁的要求吗?——“ 是我的锁,我就释放,不是我的锁,我不能瞎释放”。 其中的KEYS[1] , ARGV[1] 是参数,我们只调用 jedis 执行脚本的时候,传递这两个参数就可以了。 使用redis + lua 来实现释放锁的代码如下: private static final Long lockReleaseOK = 1L; static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// lua脚本,用来释放分布式锁 public static boolean releaseLock(String key ,String lockValue){ if(key == null || lockValue == null) { return false; } try { Jedis jedis = getJedisPool().getResource(); Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue)); jedis.close(); return res!=null && res.equals(lockReleaseOK); } catch (Exception e) { return false; } } 如此,我们便实现了锁的安全释放。同时,我们还需要结合业务逻辑,进行具体健壮性的保证,比如如果结束了一定不能忘记释放锁,异常了也要释放锁,某种情况下是否需要回滚事务等。总结这个分布式锁使用的过程便是:
用redis做分布式锁真的靠谱吗上面的文字中,我们讨论如何使用redis作为分布式锁,并讨论了一些细节问题,如锁超时的问题、安全释放锁的问题。目前为止,似乎很完美的解决的我们想要的分布式锁功能。然而事情并没有这么简单,用redis做分布式锁并不“靠谱”。 不靠谱的情况上面我们说的是redis,是单点的情况。如果是在redis sentinel集群中情况就有所不同了。关于redis sentinel 集群可以看这里。在redis sentinel集群中,我们具有多台redis,他们之间有着主从的关系,例如一主二从。我们的set命令对应的数据写到主库,然后同步到从库。当我们申请一个锁的时候,对应就是一条命令 redlock为了解决故障转移情况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,需要多个redis实例,加锁的时候,它会想多半节点发送 redlock确实解决了上面所说的“不靠谱的情况”。但是,它解决问题的同时,也带来了代价。你需要多个redis实例,你需要引入新的库 代码也得调整,性能上也会有损。所以,果然是不存在“完美的解决方案”,我们更需要的是能够根据实际的情况和条件把问题解决了就好。 至此,我大致讲清楚了redis分布式锁方面的问题(日后如果有新的领悟就继续更新)。 锁和分布式锁锁是什么?锁是一种可以封锁资源的东西。这种资源通常是共享的,通常会发生使用竞争的。 为什么需要锁?需要保护共享资源正常使用,不出乱子。 如果这个时候厕所门前有个锁,每个人都没法随便进入,而是需要先得到锁,才能进去。而得到这个锁,就需要里边的人先出来。这样就可以保证同一时刻,只有一个人在使用厕所,这个人在上厕所的期间不会有不安全的事情发生,不会中途被人闯进来了。 Java中的锁在 java 编码的时候,为了保护共享资源,使得多线程环境下,不会出现“不好的结果”。我们可以使用锁来进行线程同步。于是我们可以根据具体的情况使用synchronized 关键字来修饰一个方法,或者一段代码。这个方法或者代码就像是前文中提到的“受保护的厕所,加锁的厕所”。也可以使用 java 5以后的 Lock 来实现,与 synchronized 关键字相比,Lock 的使用更灵活,可以有加锁超时时间、公平性等优势。 分布式锁上面我们所说的 synchronized 关键字也好,Lock 也好。其实他们的作用范围是啥,就是当前的应用啊。你的代码在这个 jar 包或者这个 war 包里边,被部署在 A 机器上。那么实际上我们写的 synchronized 关键字,就是在当前的机器的 JVM在执行代码的时候发生作用的。假设这个代码被部署到了三台机器上 A,B,C。那么 A 机器中的部署的代码中的synchronized 关键字并不能控制 B,C 中的内容。 假如我们需要在 A,B,C 三台机器上运行某段程序的时候,实现“原子操作”,synchronized 关键字或者 Lock 是不能满足的。很显然,这个时候我们需要的锁,是需要协同这三个节点的,于是,分布式锁就需要上场了,他就像是在A,B,C的外面加了一个层,通过它来实现锁的控制。 redis 如何实现加锁在redis中,有一条命令,可以实现类似 “锁” 的语法是这样的: SETNX key value 他的作用是,将 使用 redis 来实现锁的逻辑就是这样的 线程 1 获取锁 -- > setnx mylock lockvalue -- > 1 获取锁成功 线程 2 获取锁 -- > setnx mylock lockvalue -- > 0 获取锁失败 (继续等待,或者其他逻辑) 线程 1 释放锁 -- > 线程 2 获取锁 -- > setnx mylock lockvalue -- > 1 获取成功 锁超时在这个例子中,我们梳理了使用 redis setnx 命令 来实现锁的逻辑。这里还需要考虑的是,锁超时的问题 ,因为当线程 1 获取了锁之后,如果业务逻辑执行很长很长时间,那么其他线程只能死等,这可不行。所以需要加上超时,结合这些考虑的情况,实际的 Java 代码可以这样写: public static boolean lock(String key,String lockValue,int expire){ if(null == key){ return false; } try { Jedis jedis = getJedisPool().getResource(); String res = jedis.set(key,lockValue,"NX","EX",expire); jedis.close(); return res!=null && res.equals("OK"); } catch (Exception e) { return false; } } retry这里执行加锁,不一定能成功。当别人正在持有锁的时候,加锁的线程需要继续尝试。这个“继续尝试”通常是“忙等待”,实现代码如下: /** * 获取一个分布式锁 , 超时则返回失败 * @param key 锁的key * @param lockValue 锁的value * @param timeout 获取锁的等待时间,单位为 秒 * @return 获锁成功 - true | 获锁失败 - false */ public static boolean tryLock(String key,String lockValue,int timeout,int expire){ final long start = System.currentTimeMillis(); if(timeout > expiredNx) { timeout = expiredNx; } final long end = start + timeout * 1000; boolean res = false; // 默认返回失败 while(!(res = lock(key,lockValue,expire))){ // 调用了上面的 lock方法 if(System.currentTimeMillis() > end) { break; } } return res; }
redis 如何释放锁根据上面所述,我们在加锁的时候执行了: 不该释放的锁但是,直接执行
因此,我们需要安全的释放锁——“不是我的锁,我不能瞎释放”。所以,我们在加锁的时候,就需要标记“这是我的锁”,在释放的时候在判断 “ 这是不是我的锁?”。这里就需要在释放锁的时候加上逻辑判断,合理的逻辑应该是这样的: 1. 线程1 准备释放锁 , 锁的key 为 mylock 锁的 value 为 thread1_magic_num 2. 查询当前锁 current_value = get mylock 3. 判断 if current_value == thread1_magic_num -- > 是 我(线程1)的锁 else -- >不是 我(线程1)的锁 4. 是我的锁就释放,否则不能释放(而是执行自己的其他逻辑)。
为了实现上面这个逻辑,我们是无法通过 redis 自带的命令直接完成的。如果,再写复杂的代码去控制释放锁,则会让整体代码太过于复杂了。所以,我们引入了lua脚本。结合Lua 脚本实现释放锁的功能,更简单,redis 执行lua脚本也是原子的,所以更合适,让合适的人干合适的事,岂不更好。 通过Lua脚本实现锁释放Lua是啥,Lua是一种功能强大,高效,轻量级,可嵌入的脚本语言。其官方的描述是: Lua is a powerful, efficient, lightweight, embeddable scripting language. It supports procedural programming, object-oriented programming, functional programming, data-driven programming, and data description. Lua 调用 redis 非常简单,并且 Lua 脚本语法也易学,对于有别的编程语言基础的程序员来说,在不学习Lua脚本语法的情况下,直接看 Lua 的代码 也是可以看懂的。例子如下: if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end 上面的代码,逻辑很简单,if 中的比较如果是true , 那么 执行 del 并返回del结果;如果 if 结果为false 直接返回 0 。这不就满足了我们释放锁的要求吗?——“ 是我的锁,我就释放,不是我的锁,我不能瞎释放”。 其中的KEYS[1] , ARGV[1] 是参数,我们只调用 jedis 执行脚本的时候,传递这两个参数就可以了。 使用redis + lua 来实现释放锁的代码如下: private static final Long lockReleaseOK = 1L; static String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// lua脚本,用来释放分布式锁 public static boolean releaseLock(String key ,String lockValue){ if(key == null || lockValue == null) { return false; } try { Jedis jedis = getJedisPool().getResource(); Object res =jedis.eval(luaScript,Collections.singletonList(key),Collections.singletonList(lockValue)); jedis.close(); return res!=null && res.equals(lockReleaseOK); } catch (Exception e) { return false; } } 如此,我们便实现了锁的安全释放。同时,我们还需要结合业务逻辑,进行具体健壮性的保证,比如如果结束了一定不能忘记释放锁,异常了也要释放锁,某种情况下是否需要回滚事务等。总结这个分布式锁使用的过程便是:
用redis做分布式锁真的靠谱吗上面的文字中,我们讨论如何使用redis作为分布式锁,并讨论了一些细节问题,如锁超时的问题、安全释放锁的问题。目前为止,似乎很完美的解决的我们想要的分布式锁功能。然而事情并没有这么简单,用redis做分布式锁并不“靠谱”。 不靠谱的情况上面我们说的是redis,是单点的情况。如果是在redis sentinel集群中情况就有所不同了。关于redis sentinel 集群可以看这里。在redis sentinel集群中,我们具有多台redis,他们之间有着主从的关系,例如一主二从。我们的set命令对应的数据写到主库,然后同步到从库。当我们申请一个锁的时候,对应就是一条命令 redlock为了解决故障转移情况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,需要多个redis实例,加锁的时候,它会想多半节点发送 redlock确实解决了上面所说的“不靠谱的情况”。但是,它解决问题的同时,也带来了代价。你需要多个redis实例,你需要引入新的库 代码也得调整,性能上也会有损。所以,果然是不存在“完美的解决方案”,我们更需要的是能够根据实际的情况和条件把问题解决了就好。 至此,我大致讲清楚了redis分布式锁方面的问题(日后如果有新的领悟就继续更新)。 |
请发表评论