当前位置: 首页 > news >正文

redis分布式锁在高并发场景下的方案设计与性能提升

引子

在上文的结尾中我提到了redis分布式锁在“主从架构”下失效的情况:比如当redis执行相应命令时,主节点挂掉了,从节点被选为新的主节点,但命令还没来得及同步到从节点,因此高并发场景下,新的请求又会拿到锁,但前一个锁并没有手动释放掉,到过期时间后,就把新请求的锁给释放掉了,那么就又出现并发问题了,本篇文章就将以解决这个问题作为开端来展开。

解决问题

在解决问题之前,我们先要认识一个名词-“CAP”。它是 Consistency(一致性)、Availability(可用性)和 Partition tolerance(分区容忍性)的缩写,这是一个在设计分布式系统时必须考虑的理论。CAP理论指出,一个分布式系统不可能同时满足这三个特性。在不存在网络失败的情况下(分布式系统正常运行时),C和A能够同时保证。只有当网络发生分区或失败时,才会在C和A之间做出选择。对于一个分布式系统而言,P是前提,必须保证,因为只要有网络交互就一定会有延迟和数据丢失,这种状况我们必须接受,必须保证系统不能挂掉。所以只剩下C、A可以选择。

为什么要先科普这个理论呢?因为redis在“主从架构”的前提下遵循的是AP,即保证高可用,也就是说主节点在收到命令执行后,会立即返回执行结果,而不是先往从节点同步数据,这也就是为什么redis主从架构下“锁”依然可能失效的原因。既然问题找到了,我们怎么解决呢?最简单的办法不用redis。

1.zookeeper分布式锁

zookeeper就不过多介绍了,不知道的同学自行百度,使用它的原因在于它追求的是CP,即数据一致性。和redis不同的是:它在接收到线程的命令后,并不会立即返回,而是先给从节点同步数据,从节点同步成功后会返回给主节点,主节点会统计从节点同步数据成功的个数,只有当半数以上的从节点同步成功,主节点才会返回“加锁成功”。

而当主节点挂掉后,从节点重新选举则依靠ZAB机制(Ps:用于实现分布式一致的协议),它通过底层的“选举算法”可以保证选举出来的从节点一定是同步过数据的。具体来说:

在ZooKeeper中,每个事务请求都会被赋予一个全局唯一的事务id,也就是zxid(ZooKeeper Transaction Id)。当主节点向从节点同步数据时,这些数据变更会作为一个事务被赋予一个zxid。
因此,如果一个从节点成功地从主节点那里接收并应用了这些数据变更,那么它的zxid就会更新。这就意味着,同步过数据的节点和未同步过数据的节点的zxid是不一样的。而在选举新的主节点时,ZooKeeper会选择zxid最大的节点,也就是数据最新的节点作为新的主节点。因为zxid最大的节点最有可能是已经同步过最新数据的节点。

2.Redlock

Redlock一种基于Redis实现的分布式锁算法,用来解决Redis分布式锁失效的问题,在使用多个独立Redis实例的情况下,能够提供更高的可靠性和安全性。
在这里插入图片描述
Redlock算法的核心思想是:使用多个独立的Redis实例作为锁服务器,当客户端获取锁和释放锁时,需要在所有的Redis实例上设置和释放。只有当超过半数以上的实例都设置或释放锁成功时,才认为操作成功,可以看得出和zookeeper的那套还是蛮像的。那么redLock怎么用呢?很简单,redisson为我们提供了封装,不需要我们动手实现,代码示例如下:

String lockKey = "lock:product:001";
//获取锁对象
RLock redissonLock1 = redisson.getLock(lockKey);
RLock redissonLock2 = redisson.getLock(lockKey);
RLock redissonLock3 = redisson.getLock(lockKey);
//将多个RLock对象关联为一个红锁
RedissonRedLock redissonLock = new RedissonRedLock(redissonLock1, redissonLock2, redissonLock3);
//加锁,如果没有手动解开的话,10秒钟后将会自动解开
redissonLock.lock(10,TimeUnit.SECONDS);

RedissonRedLock对象实现了Redlock,该对象还可以将多个RLock对象关联为一个红锁,每个RLock对象实例还可以来自于不同的Redisson实例。另外,Redisson 高版本会根据redisClient的模式来决定getLock返回的锁类型,如果集群模式,满足红锁的条件,则会直接返回红锁。

可能碰到的“坑”

1.加从节点

回到上文中的架构图,可能有的同学会觉得你这样不够“高可用”,我要给每个redis再加个从节点来保证“高可用”,这样属实是“多此一举”了,并且让我们前面解决的“超卖问题”又出现了,因为你有向从节点同步数据这一步,就有锁没及时同步的风险。其实,我们现在的架构就已经满足“高可用”的条件了,已经有3个节点了,挂1个还有2个,担心还不够安全,那就多加几个对等的节点就好了。但也不建议加太多的机器,因为更多的机器在redlock这套算法下意味着需要处理更多的请求,会影响性能,加到5台都已经支持同时挂两个节点了。

2.加对等节点

再抛出一个小问题,那我们可以只加一台机器,也就是组四个节点吗?答案是当然可以,但不建议这么做,通常我们建议redis的节点数控制为奇数个。这是为什么呢?大家想想,在redlock这套算法下,因为要保证过半数同步成功,所以三个节点最高支持挂一个,四个节点也是最高支持挂一个,那么从可用性的角度来看,三个节点和四个节点没区别,但是多了一个节点,请求要多处理一个,你还要多掏一台机器的钱,显然是没必要的。

3.redis持久化

redis有两种持久化的方式:RDB和AOF。它们的区别在于前者只管结果,后者只管过程。我们这里以AOF为例,AOF有三张持久化策略:Always、Everysec、No。我们以Everysec为例,即每秒执行一次同步操作,这也是大多数公司采用的同步策略,毕竟Always同步每条命令会带来性能问题,业务体量每到一定程度Everysec就够用了。假如在使用Everysec持久化的时候,机器挂掉了或者重启了,持久化失败了,重启后的“锁”肯定是没加上的,这时候新的请求进来了,又会回到“超卖问题”了。

性能优化

1.减少粒度

那么解决完“加锁”的问题之后,我们肯定要继续做深入地优化,比如怎么提升分布式锁的性能,支持更高的并发。首先,我们需要先明确,只要是谈及锁性能的优化,首先要想到的是一个词-“粒度”,减少锁的粒度是最先想到的方案。比如我们上面这段下单减库存的示例:

@PostMapping("/deduct_stock/red-lock")
public String deductStockRedLock() {String lockKey = "lock:product:001";//获取锁对象RLock redissonLock1 = redisson.getLock(lockKey);RLock redissonLock2 = redisson.getLock(lockKey);RLock redissonLock3 = redisson.getLock(lockKey);//将多个RLock对象关联为一个红锁RedissonRedLock redissonLock = new RedissonRedLock(redissonLock1, redissonLock2, redissonLock3);//加锁,如果没有手动解开的话,10秒钟后将会自动解开redissonLock.lock(10,TimeUnit.SECONDS);try {int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));if (stock > 0) {int realStock = stock - 1;stringRedisTemplate.opsForValue().set("stock", String.valueOf(realStock));log.info("扣减成功,剩余库存:" + realStock);} else {log.info("扣减失败,库存不足");}} finally {//释放锁redissonLock.unlock();}return "OK";
}

当然,实际业务中肯定逻辑会很复杂,所以我们需要做的就是梳理出其中不需要加锁的逻辑,把它们提出来减少锁的粒度。

2.分段锁

我们还以下单减库存这个场景为例,假如我们有100件库存,我们在库存阶段把每十件商品作为一个库存单位来设置key,当然这就是十把锁了,大家想想原本获取库存这一步从一把锁变成了十把锁,那么同一时刻处理的请求数就由一个变为十个了,性能瞬间提升十倍。分段锁的思想就是拆分业务逻辑,根据拆分情况加不同的锁从而提升并发和性能,当然提升性能的代价就是需要在找库存这步多写点逻辑进行处理了。

从场景谈分布式锁的应用

1.下单重复提交

这个场景很常见,前后端都有相应的方案。首先,前端肯定要加“防抖”,后端则需要做幂等, 幂等的方案也很多,我们这里就谈谈分布式锁在该场景怎么做。很简单,我们只需要在下单的业务逻辑前加锁,但锁的key在这里有说法的,肯定不能用订单id,因为每次下单都会产生一条新的订单,所以我们这里用用户id作为key更为合适。

2.支付与取消订单同时发生

很多电商网站的订单如果你在一定的时间内未支付的话,会自动取消,那如果你即将在取消订单前付款,在高并发场景下,就有可能发生支付了一个已经取消的订单,用户付款了却永远也收不到货,那势必会带来问题。我们这个场景也是可以用分布式锁来解决的,这两个业务逻辑前都加上分布式锁,就用订单id作为key,这样两个操作一次只能执行一个。

http://www.xdnf.cn/news/682255.html

相关文章:

  • 晓辉教育五维乾坤:五个成语解码教育范式革命
  • mysql explain使用
  • 图片压缩工具 | Electron+Vue3+Rsbuild开发桌面应用
  • SecureCRT 和 MobaXterm 用于串口收发时数据异常(无法成功发送)——更改换行符解决
  • OpenResty 入门指南:从基础到动态路由实战
  • 第1章 Redis 概述
  • Java基础 Day22
  • python变量如何理解?
  • 图像分割全路线学习(结合论文)
  • go build -gcflags 参数学习
  • Spring Boot事务失效场景及解决方案
  • 自动驾驶决策规划框架详解:从理论到实践
  • 【C语言干货】回调函数
  • endnote2025安装教程以及激活文件
  • ELectron 中 BrowserView 如何进行实时定位和尺寸调整
  • Asp.Net Core 如何配置在Swagger中带JWT报文头
  • leetcode hot100刷题日记——21.不同路径
  • 六、西方哲学
  • 【连载19】基础智能体的进展与挑战综述-对智能体大脑的威胁
  • halcon高斯滤波
  • 网络编程--上篇
  • 【详细记录】我的第一次裸片硬件尝试:stm32f103c8t6最小核心板一板成
  • unet 视频截图 实现简单的unet kaggle运行
  • Kruskal-Wallis检验 vs. 多次Wilcoxon检验:多重比较-spss
  • LCR 094. 分割回文串 II
  • Elasticsearch搜索机制与分页优化策略
  • Pytest自动化测试框架搭建:Jenkins持续集成
  • 通俗解释网络参数RTT(往返时间)
  • Scratch节日 | 六一儿童节
  • 并发编程(二)—synchronized和volatile