Redis(四)大厂生产级Redis高并发分布式锁实战【Redission分布式锁 升级过程(解决分布式锁-解决死锁-解决锁误删-解决锁续命)】【Redisso

@TOC

转自blog.csdn.net/qq_32681589...

一个分布式案例引发的思考

先假设,我们当前线上有一个项目,使用nginx分别轮循到2个tomcat上。它的模型如下: 如上图,为了减缓节点压力,我们把项目部署成了2个tomcat,分别是8080端口和8081端口。并且采用的是轮询策略,客户端每次过来一条请求,将按序依次分流到这2个tomcat上。 然后,这个tomcat项目提供了如下这个接口:

java 复制代码
@RequestMapping("/deduct_stock")
public String deductStock1() {
    int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
    if (stock > 0) {
        int realStock = stock - 1;
        stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
        System.out.println("扣减成功,剩余库存:" + realStock);
    } else {
        System.out.println("扣减失败,库存不足");
    }
    return "end";
}

1. Redissetnx解决

案例分析:这个案例很简单,就是提供一个扣减库存的接口,模拟外部电商系统购买物品之后,扣减库存。但是,大家一定要注意到以下几个点:

  1. 无论我们部署多少个tomcat,库存肯定是被共享的。假设,我只有100个优惠产品,那肯定只能被卖出去100件
  2. 我们把库存量放到了redis中去了,每卖出去一件,stock - 1,并且写回缓存

但是两个tomcat就会出现并发问题,并且用synchronized不能解决两个tomcat问题

改正后的代码如下:

java 复制代码
    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
		
		// 使用redis的setnx命令
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
        if (!result) {
            return "error_code";
        }

		try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
             stringRedisTemplate.delete(lockKey);
        }
        return "end";
    }

这边很简单,就是使用了stringRedisTemplate.opsForValue().setIfAbsent,即:Redissetnx命令。然后,如果Redis返回的result不是true,那就返回一个错误码,提示客户端【上锁失败】就好了。那同学们,这样就行了吗?我们画个图吧,嘿嘿嘿

但是会出现如下问题: 就像上图这样,显然,从目前来看,是没问题,确实已经实现了,多个tomcat情况下,都能控制共享资源了。但是,万一,真的出现了客户端1在拿到锁之后,还没走到释放锁的代码就宕机了,那完了,资源没办法被释放!怎么办?难道我手动删除不成?这就是,单纯利用setnx会遇到的第一个问题:==死锁。==

2. 解决Redissetnx死锁

setnx + 过期时间

java 复制代码
    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge");
        stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

		try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
             stringRedisTemplate.delete(lockKey);
        }

        return "end";
    }

跟上述代码一样,我们在setIfAbsent之后,加一个过期时间函数expire。这个方案其实是不行的。很显然,目前这两步操作不是【原子性】的,Java代码嘛,肯定是一条一条按顺序执行的,就跟上面的例子一样,当我们出现极端情况,诶,==还真就执行完setIfAbsent之后,expire之前宕机了呢?一样完犊子==,会出现死锁,所以,正常我们是利用setIfAbsent另一个重载方法,它会帮我们【原子】地操作这两步,如下:

3. 解决设置过期时间语句 的原子性问题

java 复制代码
    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "zhuge", 30, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
             stringRedisTemplate.delete(lockKey);
        }

        return "end";
    }

好了,目前是原子性的了。并且呢,我们给这个锁,+了一个30秒的过期时间。可以了吗?啊,不完全对。大家能想明白吗? 很显然啊,==你这个过期时间是固定30秒的,万一我业务30秒内完成不了呢==?嘿,你是不是想说什么业务30秒完成不了,哈,真可能出现,比如IO阻塞了什么的。 那有朋友会继续建议:那我设置60秒?120秒?240秒?丢,我设置超长时间,总行了吧???咳咳咳,啊这个,有点道理的 由于客户端A在执行业务期间锁就过期了,此时,客户端B进来加锁肯定是能成功的。但是客户端A在没有出现错误的情况,肯定会继续执行下去的,并且最终会释放锁。那最终这个释放锁释放的是谁的锁呢?客户端B的呀!此时,又有一个新的客户端C过来加锁,那不是成功了吗?显然这样做是有问题的,【错误释放别人的锁】,并且自己的业务还不一定执行完了!

4. 解决锁误删问题

其实针对这个问题,还是有解决方案的。那就是每次上锁的时候,+一个uuid,==最后释放锁的时候判断一下uuid是不是跟当前的uuid一样就好了==。如下:

java 复制代码
    @RequestMapping("/deduct_stock")
    public String deductStock1() {
        String lockKey = "lock:product_101";
        String clientId = UUID.randomUUID().toString();
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
        if (!result) {
            return "error_code";
        }

        try {
            int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
            if (stock > 0) {
                int realStock = stock - 1;
                stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
                System.out.println("扣减成功,剩余库存:" + realStock);
            } else {
                System.out.println("扣减失败,库存不足");
            }
        } finally {
            if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
                stringRedisTemplate.delete(lockKey);
            }
        }
        return "end";
    }

关键代码如上:最后finaly块判断释放的时候,里面的value值是不是当初我们设置的那个。但其实这仅仅只是解决了我们其中一个问题而已。还有个关键的【拒绝服务】问题呢。追根揭底,还是【锁时间】到底该如何确定的问题。 于是有人提出了一个方案:锁续命。顾名思义,就是设置一个相对不那么长的时间,但是临到期前或者某个时间点,重新设置过期时间。

5. 解决锁续命问题

Redisson实现分布式锁:setnx + 过期时间 + 锁续命

xml 复制代码
<dependency>
  <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
 </dependency>

 <dependency>
   <groupId>redis.clients</groupId>
   <artifactId>jedis</artifactId>
   <version>2.9.0</version>
 </dependency>

 <dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
 </dependency>
java 复制代码
@RequestMapping("/deduct_stock")
public String deductStock1() {

     String lockKey = "lock:product_001";
     
     //获取锁对象
     RLock redissonLock = redisson.getLock(lockKey);
     
     //加分布式锁
     redissonLock.lock();  //  .setIfAbsent(lockKey, clientId, 30, TimeUnit.SECONDS);
     try {
         int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); 
         if (stock > 0) {
             int realStock = stock - 1;
             stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
             System.out.println("扣减成功,剩余库存:" + realStock);
         } else {
             System.out.println("扣减失败,库存不足");
         }
     } finally {
         
         //解锁
         redissonLock.unlock();
     }
     return "end";
 }

==上面的代码很简单,我们无需关心之前提到的那几个问题了,Redisson在封装的api里面已经帮我们做好了一切。我们只需要简单的调用lock跟unlock而已。==

Redisson客户端实现的分布式锁源码分析

本次源码分析的入口,就是【3.3】中最后给出的代码示例redissonLock.lock()。为了方便大家理解,这里我们给出这个源码实现的原理图:

相关推荐
yang-23071 分钟前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code6 分钟前
(Django)初步使用
后端·python·django
代码之光_198013 分钟前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长26 分钟前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记1 小时前
DataX+Crontab实现多任务顺序定时同步
后端
姜学迁2 小时前
Rust-枚举
开发语言·后端·rust
爱学习的小健3 小时前
MQTT--Java整合EMQX
后端
北极小狐3 小时前
Java vs JavaScript:类型系统的艺术 - 从 Object 到 any,从静态到动态
后端
tangdou3690986553 小时前
两种方案手把手教你多种服务器使用tinyproxy搭建http代理
运维·后端·自动化运维
【D'accumulation】3 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端