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

简介: 本文探讨了Redis分布式锁在主从架构下失效的问题及其解决方案。首先通过CAP理论分析,Redis遵循AP原则,导致锁可能失效。针对此问题,提出两种解决方案:Zookeeper分布式锁(追求CP一致性)和Redlock算法(基于多个Redis实例提升可靠性)。文章还讨论了可能遇到的“坑”,如加从节点引发超卖问题、建议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实例的情况下,能够提供更高的可靠性和安全性。

1.png

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,这样两个操作一次只能执行一个。

目录
相关文章
|
1月前
|
关系型数据库 MySQL 分布式数据库
Super MySQL|揭秘PolarDB全异步执行架构,高并发场景性能利器
阿里云瑶池旗下的云原生数据库PolarDB MySQL版设计了基于协程的全异步执行架构,实现鉴权、事务提交、锁等待等核心逻辑的异步化执行,这是业界首个真正意义上实现全异步执行架构的MySQL数据库产品,显著提升了PolarDB MySQL的高并发处理能力,其中通用写入性能提升超过70%,长尾延迟降低60%以上。
|
29天前
|
缓存 NoSQL 算法
高并发秒杀系统实战(Redis+Lua分布式锁防超卖与库存扣减优化)
秒杀系统面临瞬时高并发、资源竞争和数据一致性挑战。传统方案如数据库锁或应用层锁存在性能瓶颈或分布式问题,而基于Redis的分布式锁与Lua脚本原子操作成为高效解决方案。通过Redis的`SETNX`实现分布式锁,结合Lua脚本完成库存扣减,确保操作原子性并大幅提升性能(QPS从120提升至8,200)。此外,分段库存策略、多级限流及服务降级机制进一步优化系统稳定性。最佳实践包括分层防控、黄金扣减法则与容灾设计,强调根据业务特性灵活组合技术手段以应对高并发场景。
427 7
|
2月前
|
人工智能 负载均衡 Java
Spring AI Alibaba 发布企业级 MCP 分布式部署方案
本文介绍了Spring AI Alibaba MCP的开发与应用,旨在解决企业级AI Agent在分布式环境下的部署和动态更新问题。通过集成Nacos,Spring AI Alibaba实现了流量负载均衡及节点变更动态感知等功能。开发者可方便地将企业内部业务系统发布为MCP服务或开发自己的AI Agent。文章详细描述了如何通过代理应用接入存量业务系统,以及全新MCP服务的开发流程,并提供了完整的配置示例和源码链接。未来,Spring AI Alibaba计划结合Nacos3的mcp-registry与mcp-router能力,进一步优化Agent开发体验。
1093 14
|
2月前
|
存储 NoSQL Java
从扣减库存场景来讲讲redis分布式锁中的那些“坑”
本文从一个简单的库存扣减场景出发,深入分析了高并发下的超卖问题,并逐步优化解决方案。首先通过本地锁解决单机并发问题,但集群环境下失效;接着引入Redis分布式锁,利用SETNX命令实现加锁,但仍存在死锁、锁过期等隐患。文章详细探讨了通过设置唯一标识、续命机制等方法完善锁的可靠性,并最终引出Redisson工具,其内置的锁续命和原子性操作极大简化了分布式锁的实现。最后,作者剖析了Redisson源码,揭示其实现原理,并预告后续关于主从架构下分布式锁的应用与性能优化内容。
126 0
|
4月前
|
NoSQL Redis
Redis分布式锁如何实现 ?
Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。 只有在key不存在的情况下,将键key的值设置为value。如果key已经存在,则SETNX命令不做任何操作。 这个命令的返回值如下。 ● 命令在设置成功时返回1。 ● 命令在设置失败时返回0。 假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行S
|
NoSQL Redis 数据库
用redis实现分布式锁时容易踩的5个坑
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! 近有不少小伙伴投入短视频赛道,也出现不少第三方数据商,为大家提供抖音爬虫数据。 小伙伴们有没有好奇过,这些数据是如何获取的,普通技术小白能否也拥有自己的抖音爬虫呢? 本文会全面解密抖音爬虫的幕后原理,不需要任何编程知识,还请耐心阅读。
用redis实现分布式锁时容易踩的5个坑
|
NoSQL Java 关系型数据库
浅谈Redis实现分布式锁
浅谈Redis实现分布式锁
|
存储 canal 缓存
|
NoSQL PHP Redis
redis实现分布式锁
redis实现分布式锁
259 0
redis实现分布式锁
|
消息中间件 NoSQL Java
基于Redis实现分布式锁
基于Redis实现分布式锁
367 0
基于Redis实现分布式锁