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

目录
相关文章
|
2月前
|
存储 负载均衡 NoSQL
【赵渝强老师】Redis Cluster分布式集群
Redis Cluster是Redis的分布式存储解决方案,通过哈希槽(slot)实现数据分片,支持水平扩展,具备高可用性和负载均衡能力,适用于大规模数据场景。
207 2
|
2月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
121 6
|
3月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
13天前
|
存储 监控 NoSQL
Redis高可用架构全解析:从主从复制到集群方案
Redis高可用确保服务持续稳定,避免单点故障导致数据丢失或业务中断。通过主从复制实现数据冗余,哨兵模式支持自动故障转移,Cluster集群则提供分布式数据分片与水平扩展,三者层层递进,保障读写分离、容灾切换与大规模数据存储,构建高性能、高可靠的Redis架构体系。
|
14天前
|
存储 缓存 NoSQL
Redis持久化深度解析:数据安全与性能的平衡艺术
Redis持久化解决内存数据易失问题,提供RDB快照与AOF日志两种机制。RDB恢复快、性能高,但可能丢数据;AOF安全性高,最多丢1秒数据,支持多种写回策略,适合不同场景。Redis 4.0+支持混合持久化,兼顾速度与安全。根据业务需求选择合适方案,实现数据可靠与性能平衡。(238字)
|
28天前
|
NoSQL Java 调度
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
分布式锁是分布式系统中用于同步多节点访问共享资源的机制,防止并发操作带来的冲突。本文介绍了基于Spring Boot和Redis实现分布式锁的技术方案,涵盖锁的获取与释放、Redis配置、服务调度及多实例运行等内容,通过Docker Compose搭建环境,验证了锁的有效性与互斥特性。
分布式锁与分布式锁使用 Redis 和 Spring Boot 进行调度锁(不带 ShedLock)
|
22天前
|
缓存 NoSQL 关系型数据库
Redis缓存和分布式锁
Redis 是一种高性能的键值存储系统,广泛用于缓存、消息队列和内存数据库。其典型应用包括缓解关系型数据库压力,通过缓存热点数据提高查询效率,支持高并发访问。此外,Redis 还可用于实现分布式锁,解决分布式系统中的资源竞争问题。文章还探讨了缓存的更新策略、缓存穿透与雪崩的解决方案,以及 Redlock 算法等关键技术。
|
3月前
|
NoSQL Redis
Lua脚本协助Redis分布式锁实现命令的原子性
利用Lua脚本确保Redis操作的原子性是分布式锁安全性的关键所在,可以大幅减少由于网络分区、客户端故障等导致的锁无法正确释放的情况,从而在分布式系统中保证数据操作的安全性和一致性。在将这些概念应用于生产环境前,建议深入理解Redis事务与Lua脚本的工作原理以及分布式锁的可能问题和解决方案。
141 8
|
2月前
|
监控 NoSQL 关系型数据库
保障Redis与MySQL数据一致性的强化方案
在设计时,需要充分考虑到业务场景和系统复杂度,避免为了追求一致性而过度牺牲系统性能。保持简洁但有效的策略往往比采取过于复杂的方案更加实际。同时,各种方案都需要在实际业务场景中经过慎重评估和充分测试才可以投入生产环境。
123 0
|
4月前
|
关系型数据库 MySQL 分布式数据库
Super MySQL|揭秘PolarDB全异步执行架构,高并发场景性能利器
阿里云瑶池旗下的云原生数据库PolarDB MySQL版设计了基于协程的全异步执行架构,实现鉴权、事务提交、锁等待等核心逻辑的异步化执行,这是业界首个真正意义上实现全异步执行架构的MySQL数据库产品,显著提升了PolarDB MySQL的高并发处理能力,其中通用写入性能提升超过70%,长尾延迟降低60%以上。

热门文章

最新文章