当前位置:   article > 正文

Redis实现分布式锁_redis set setparams

redis set setparams

在之前并发系列的文章中,我们介绍了JVM中的锁。但是无论是synchronized还是Lock,都运行在线程级别上,必须运行在同一个JVM中。如果竞争资源的进程不在同一个JVM中时,这样线程锁就无法起到作用,必须使用分布式锁来控制多个进程对资源的访问。

 

分布式锁的实现一般有三种方式,使用MySql数据库行锁,基于Redis的分布式锁,以及基于Zookeeper的分布式锁。本文中我们重点看一下Redis如何实现分布式锁。

首先,看一下用于实现分布式锁的两个Redis基础命令:

setnx key value

这里的setnx,是"set if Not eXists"的缩写,表示当指定的key值不存在时,为key设定值为value。如果key存在,则设定失败。

setex key timeout value

setex命令为指定的key设置值及其过期时间(以秒为单位)。如果key已经存在,setex命令将会替换旧的值。

基于这两个指令,我们能够实现:

  • 使用setnx 命令,保证同一时刻只有一个线程能够获取到锁

  • 使用setex 命令,保证锁会超期释放,从而不因一个线程长期占有一个锁而导致死锁。

这里将两个命令结合在一起使用的原因是,在正常情况下,如果只使用setnx 命令,使用完成后使用delete命令删除锁进行释放,不存在什么问题。但是如果获取分布式锁的线程在运行中挂掉了,那么锁将不被释放。如果使用setex 设置了过期时间,即使线程挂掉,也可以自动进行锁的释放。

手写Redis分布式锁

接下来,我们基于Redis+Spring手写实现一个分布式锁。首先配置Jedis连接池:

  1. @Configuration
  2. public class Config {
  3. @Bean
  4. public JedisPool jedisPool(){
  5. JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
  6. jedisPoolConfig.setMaxIdle(100);
  7. jedisPoolConfig.setMinIdle(1);
  8. jedisPoolConfig.setMaxWaitMillis(2000);
  9. jedisPoolConfig.setTestOnBorrow(true);
  10. jedisPoolConfig.setTestOnReturn(true);
  11. JedisPool jedisPool=new JedisPool(jedisPoolConfig,"127.0.0.1",6379);
  12. return jedisPool;
  13. }
  14. }

实现RedisLock分布式锁:

  1. public class RedisLock implements Lock {
  2. @Autowired
  3. JedisPool jedisPool;
  4. private static final String key = "lock";
  5. private ThreadLocal<String> threadLocal = new ThreadLocal<>();
  6. @Override
  7. public void lock() {
  8. boolean b = tryLock();
  9. if (b) {
  10. return;
  11. }
  12. try {
  13. TimeUnit.MILLISECONDS.sleep(50);
  14. } catch (Exception e) {
  15. e.printStackTrace();
  16. }
  17. lock();//递归调用
  18. }
  19. @Override
  20. public boolean tryLock() {
  21. SetParams setParams = new SetParams();
  22. setParams.ex(10);
  23. setParams.nx();
  24. String s = UUID.randomUUID().toString();
  25. Jedis resource = jedisPool.getResource();
  26. String lock = resource.set(key, s, setParams);
  27. resource.close();
  28. if ("OK".equals(lock)) {
  29. threadLocal.set(s);
  30. return true;
  31. }
  32. return false;
  33. }
  34. //解锁判断锁是不是自己加的
  35. @Override
  36. public void unlock(){
  37. //调用lua脚本解锁
  38. String script="if redis.call(\"get\",KEYS[1]==ARGV[1] then\n"+
  39. " return redis.call(\"del\",KEYS[1])\n"+
  40. "else\n"+
  41. " return 0\n"+
  42. "end";
  43. Jedis resource = jedisPool.getResource();
  44. Object eval=resource.eval(script, Arrays.asList(key),Arrays.asList(threadLocal.get()));
  45. if (Integer.valueOf(eval.toString())==0){
  46. resource.close();
  47. throw new RuntimeException("解锁失败");
  48. }
  49. /*
  50. *不写成下面这种也是因为不是原子操作,和ex、nx相同
  51. String s = resource.get(key);
  52. if (threadLocal.get().equals(s)){
  53. resource.del(key);
  54. }
  55. */
  56. resource.close();
  57. }
  58. @Override
  59. public void lockInterruptibly() throws InterruptedException {
  60. }
  61. @Override
  62. public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
  63. return false;
  64. }
  65. @Override
  66. public Condition newCondition() {
  67. return null;
  68. }
  69. }

简单对上面代码中需要注意的地方做一解释:

  • 加锁过程中,使用SetParams 同时设置nx和ex的值,保证原子操作

  • 通过ThreadLocal保存key对应的value,通过value来判断锁是否当前线程自己加的,避免线程错乱解锁

  • 释放锁的过程中,使用lua脚本进行删除,保证Redis在执行此脚本时不执行其他操作,从而保证操作的原子性

但是,这段手写的代码可能会存在一个问题,就是不能保证业务逻辑一定能被执行完成,因为设置了锁的过期时间可能导致过期。

Redisson

基于上面存在的问题,我们可以使用Redisson分布式可重入锁。Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。

引入依赖:

  1. <dependency>
  2. <groupId>org.redisson</groupId>
  3. <artifactId>redisson</artifactId>
  4. <version>3.10.7</version>
  5. </dependency>

配置RedissonClient:

  1. @Configuration
  2. public class RedissonConfig {
  3. @Bean
  4. public RedissonClient redissonClient(){
  5. Config config=new Config();
  6. config.useSingleServer().setAddress("redis://127.0.0.1:6379");
  7. RedissonClient redissonClient= Redisson.create(config);
  8. return redissonClient;
  9. }
  10. }

下面对常用方法进行测试。方法1:

void lock();

测试接口:

  1. @GetMapping("/lock")
  2. public String test() {
  3. RLock lock = redissonClient.getLock("lock");
  4. lock.lock();
  5. System.out.println(Thread.currentThread().getName()+" get redisson lock");
  6. try {
  7. System.out.println("do something");
  8. TimeUnit.SECONDS.sleep(20);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. lock.unlock();
  13. System.out.println(Thread.currentThread().getName()+ " release lock");
  14. return "locked";
  15. }

进行测试,同时发送两个请求,redisson锁生效:

方法2:

void lock(long leaseTime, TimeUnit unit);

Redisson可以给lock()方法提供leaseTime参数来指定加锁的时间,超过这个时间后锁可以自动释放。测试接口:

  1. @GetMapping("/lock2")
  2. public String test2() {
  3. RLock lock = redissonClient.getLock("lock");
  4. lock.lock(10,TimeUnit.SECONDS);
  5. System.out.println(Thread.currentThread().getName()+" get redisson lock");
  6. try {
  7. System.out.println("do something");
  8. TimeUnit.SECONDS.sleep(20);
  9. } catch (InterruptedException e) {
  10. e.printStackTrace();
  11. }
  12. System.out.println(Thread.currentThread().getName()+ " release lock");
  13. return "locked";
  14. }

运行结果:

可以看出,在第一个线程还没有执行完成时,就释放了redisson锁,第二个线程进入后,两个线程可以同时执行被锁住的代码逻辑。这样可以实现无需调用unlock方法手动解锁。

方法3:

boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;

tryLock方法会尝试加锁,最多等待waitTime秒,上锁以后过leaseTime秒自动解锁;如果没有等待时间,锁不住直接返回false。

  1. @GetMapping("/lock3")
  2. public String test3() {
  3. RLock lock = redissonClient.getLock("lock");
  4. try {
  5. boolean res = lock.tryLock(5, 30, TimeUnit.SECONDS);
  6. if (res){
  7. try{
  8. System.out.println(Thread.currentThread().getName()+" 获取到锁,返回true");
  9. System.out.println("do something");
  10. TimeUnit.SECONDS.sleep(20);
  11. }finally {
  12. lock.unlock();
  13. System.out.println(Thread.currentThread().getName()+" 释放锁");
  14. }
  15. }else {
  16. System.out.println(Thread.currentThread().getName()+" 未获取到锁,返回false");
  17. }
  18. } catch (InterruptedException e) {
  19. e.printStackTrace();
  20. }
  21. return "lock";
  22. }

运行结果:

可见在第一个线程获得锁后,第二个线程超过等待时间仍未获得锁,返回false放弃获得锁的过程。

除了以上单机Redisson锁以外,还支持我们之前提到过的哨兵模式和集群模式,只需要改变Config的配置即可。以集群模式为例:

  1. @Bean
  2. public RedissonClient redissonClient(){
  3. Config config=new Config();
  4. config.useClusterServers().addNodeAddress("redis://172.20.5.170:7000")
  5. .addNodeAddress("redis://172.20.5.170:7001")
  6. .addNodeAddress("redis://172.20.5.170:7002")
  7. .addNodeAddress("redis://172.20.5.170:7003")
  8. .addNodeAddress("redis://172.20.5.170:7004")
  9. .addNodeAddress("redis://172.20.5.170:7005");
  10. RedissonClient redissonClient = Redisson.create(config);
  11. return redissonClient;
  12. }

RedLock红锁

下面介绍一下Redisson红锁RedissonRedLock,该对象也可以用来将多个RLock对象关联为一个红锁,每个RLock对象实例可以来自于不同的Redisson实例。

RedissonRedLock针对的多个Redis节点,这多个节点可以是集群,也可以不是集群。当我们使用RedissonRedLock时,只要在大部分节点上加锁成功就算成功。看一下使用:

  1. @GetMapping("/testRedLock")
  2. public void testRedLock() {
  3. Config config1 = new Config();
  4. config1.useSingleServer().setAddress("redis://172.20.5.170:6379");
  5. RedissonClient redissonClient1 = Redisson.create(config1);
  6. Config config2 = new Config();
  7. config2.useSingleServer().setAddress("redis://172.20.5.170:6380");
  8. RedissonClient redissonClient2 = Redisson.create(config2);
  9. Config config3 = new Config();
  10. config3.useSingleServer().setAddress("redis://172.20.5.170:6381");
  11. RedissonClient redissonClient3 = Redisson.create(config3);
  12. String resourceName = "REDLOCK";
  13. RLock lock1 = redissonClient1.getLock(resourceName);
  14. RLock lock2 = redissonClient2.getLock(resourceName);
  15. RLock lock3 = redissonClient3.getLock(resourceName);
  16. RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
  17. boolean isLock;
  18. try {
  19. isLock = redLock.tryLock(5, 30, TimeUnit.SECONDS);
  20. if (isLock) {
  21. System.out.println("do something");
  22. TimeUnit.SECONDS.sleep(20);
  23. }
  24. } catch (Exception e) {
  25. e.printStackTrace();
  26. } finally {
  27. redLock.unlock();
  28. }
  29. }

相对于单Redis节点来说,RedissonRedLock的优点在于防止了单节点故障造成整个服务停止运行的情况;并且在多节点中锁的设计,及多节点同时崩溃等各种意外情况有自己独特的设计方法。使用RedissonRedLock,性能方面会比单节点Redis分布式锁差一些,但可用性比普通锁高很多。

参考资料:

https://github.com/redisson/redisson/wiki/

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号