redis分布式锁
一:故事背景
本篇文章是redis实战系列的第二篇文章。
本章的主要内容是Redis分布式锁的相关知识。本篇文章将告诉你什么是分布式锁,结合一个业务场景,先带大家看看,单机上是如何实现锁功能的。
学完本篇,你可以了解到什么是锁,为什么要加锁。
二:什么是Redis分布式锁
Redis的分布式锁是一种用于协调分布式系统并保护共享资源的机制。它利用Redis的原子操作和单线程执行特性来保证多个进程或线程之间的互斥性。它可以保证在分布式环境中,同一时刻只有一个客户端能够获取到锁,从而避免了多个客户端同时对同一资源进行修改的问题。
三:Redis分布式锁的作用
高可用性:Redis 分布式锁可以使用 Redis 集群或者 Redis Sentinel 进行部署,保证了高可用性和可扩展性。
可重入性:可以让同一个客户端重复获得锁,避免了同一个客户端重复执行代码的问题。
自动过期:可以设置锁的过期时间,如果锁没有及时释放,就会自动过期,避免了死锁的问题。
支持阻塞和非阻塞:可以根据需要选择阻塞式或者非阻塞式的锁。
高性能:使用 Redis 自带的原子操作实现锁的获取和释放,具有高性能和高并发性。
四:业务场景
接下来我将结合一个秒杀的例子讲述如果实现Redis的分布式锁。
秒杀场景是一个非常经典的需要使用锁的场景。
假设有一个商品限时秒杀的业务场景,多个用户同时在秒杀开始时间内尝试购买该商品,但是该商品数量有限,只有一定数量的用户可以购买成功,其他用户则购买失败。
为了保证秒杀的公平性与真确性,这个时候我们就要通过锁来对商品的数量进行访问
五:代码实现
5.1 未加任何锁
结合上面的业务场景,我们来先来实现一个未加任何锁的代码,简单实现一下这个小需求,并且分析它存在的问题,这样可以更好的帮助我们理解为什么要加锁。
5.1.1 数据准备
首先在redis里添加了 key值为 stock value值 为 200 的数据,模拟我们要秒杀的商品数量为200。
5.1.2 未加锁的业务逻辑代码
@RestController @RequestMapping("/test") public class IndexController { // 自动注入 StringRedisTemplate 对象 @Autowired private StringRedisTemplate stringRedisTemplate; // 处理 HTTP GET 请求路径是 /test/lock @GetMapping("lock") public String deductStock() { // 获取当前库存 String stock1 = stringRedisTemplate.opsForValue().get("stock"); if( stock1 == null){ System.out.println("秒杀未开始"); return "end"; } int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock > 0) { // 扣减库存 int realStock = stock - 1; // 更新库存 stringRedisTemplate.opsForValue().set("stock", realStock + ""); System.out.println("扣减成功,剩余的库存为:" + realStock); } else { System.out.println("扣减失败,库存不足"); } return "end"; } }
上述是根据我们的业务进行的一个简单的实现,在这个实现里,未对代码进行加锁。如果在并发请求的时候,这段代码将会出现很经典的超卖问题。
5.1.3 接口压测,模拟并发情况
让我们来压测一下接口,看一下对应的效果在这里我们使用的ApiPost进行一键压测。发送了50个请求,让我们来一起看看请求的结果。
5.1.4 压测结果5.1.5 压测问题
我们发现,50个请求进来之后,如果是正常的情况下,是应该减少50个库存,每个请求获得1个商品。
可以根据结果看,我们的50个请求获得了5个商品。同一个商品卖给了多个用户。列如 195号商品同时卖给了10个人。
那么我们该如何去解决这个问题呢?
六:单机情况下JVM级别加锁
首先我们来看一下,如果是单机(项目只部署在一台机器上),使用 synchronized 进行jvm级别加锁,解决上述问题。
6.1加锁代码
synchronized (this){ // 获取当前库存 String stock1 = stringRedisTemplate.opsForValue().get("stock"); if( stock1 == null){ System.out.println("秒杀未开始"); return "end"; } int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); if (stock > 0) { // 扣减库存 int realStock = stock - 1; // 更新库存 stringRedisTemplate.opsForValue().set("stock", realStock + ""); System.out.println("扣减成功,剩余的库存为:" + realStock); } else { System.out.println("扣减失败,库存不足"); } }
代码非常的简单,使用 synchronized 关键字,将我们的业务逻辑进行包裹即可。
synchronized,保证同一时刻只有一个线程执行被synchronized修饰的代码块或方法,从而避免多个线程同时对共享资源进行修改而导致的数据不一致的问题。
6.2 运行结果
6.3 结果分析
从结果上来看,通过synchronized 可以在jvm级别上进行上锁。但是我们实际的生产环境中,很少有部署单机服务的。如果我们部署了多个服务,那么通过synchronized 是肯定无法影响另一条机器上的请求的。
七:多服务部署
7.1 图像展示
7.2 问题分析
假设我们,部署了两个服务,部署在tomcat1和tomcat2上,使用nginx做负载。此时仅仅通过synchronized 只能保持 tomcat1自己本身。tomcat2自己本身的数据被锁住。如果两个服务同时提供服务,仍然会产生我们上述的超卖问题。
八:总结提升
本文我们主要讲了锁的概念,为什么要加锁,单机上jvm级别的加锁,多服务部署的话,我们现在的代码存在的问题。
接下来我会讲解如何解决我们这次遗留的问题,在分布式环境下,如何加锁,如何解决可能会存在的问题。