RedLock到底要不要用?看完别再问了

为什么需要RedLock?

这一点很好理解,因为普通的分布式锁算法在加锁时它的KEY只会存在于某一个Redis Master实例以及它的slave上(假如有slave的话, 即使cluster集群模式,也是一样的。因为一个KEY只会属于一个slot,一个slot只会属于一个Redis节点),如下图所示(图中虚线表示cluster中gossip协议交互路径):

在这里插入图片描述

因为它只会存在于某一个Redis Master上,而Redis又不是基于CP模型的。那么就会有很大概率存在锁丢失的情况。以如下场景为例:

1、线程T1在M1中加锁成功。
2、M1出现故障,但是由于主从同步延迟问题,加锁的KEY并没有同步到S1上。
3、S1升级为Master节点。
4、另一个线程T2在S1上也加锁成功,从而导致线程T1和T2都获取到了分布式锁。

而RedLock方法就是根除普通基于Redis分布式锁而生的(无论是主从模式、sentinel模式还是cluster模式)!官方把RedLock方法当作使用Redis实现分布式锁的规范算法,并认为这种实现比普通的单实例或者基于Redis Cluster的实现更安全。

RedLock定义

首先,我们要掌握RedLock的第一步就是了解它的定义。这一点,官方网站肯定是最权威的。接下来的这段文字摘自 http://redis.cn/topics/distlock.html

在Redis的分布式环境中,我们假设有N个Redis Master。这些节点完全互相独立,不存在主从复制或者其他集群协调机制(这句话非常重要,如果没有理解这句话,也就无法理解RedLock。并且由这句话我们可以得出,RedLock依赖的环境不能是一个由N主N从组成的Cluster集群模式,因为Cluster模式下的各个Master并不完全独立,而是存在Gossip协调机制的)。

接下来,我们假设有3个完全相互独立的Redis Master单机节点,所以我们需要在3台机器上面运行这些实例,如下图所示(请注意这张图中3个Master节点完全相互独立):
在这里插入图片描述
为了取到锁,客户端应该执行以下操作:

  1. 获取当前Unix时间,以毫秒为单位。
  2. 依次尝试从N个Master实例使用相同的key和随机值获取锁(假设这个key是LOCK_KEY)。当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
  3. 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
  4. 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
  5. 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。

基于Redisson实现RedLock

RedLock方案并不是很复杂,但是如果我们自己去实现一个工业级的RedLock方案还是有很多坑的。幸运的是,Redisson已经为我们封装好了RedLock的开源实现,假设基于3个单机Redis实例实现RedLock分布式锁,即第二张图所示的RedLock方案,其源码如下所示,非常简单:

Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.0.1:6379")
        .setPassword("afeiblog").setDatabase(0);
RedissonClient redissonClient1 = Redisson.create(config1);

Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.0.2:6379")
        .setPassword("afeiblog").setDatabase(0);
RedissonClient redissonClient2 = Redisson.create(config2);

Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.0.3:6379")
        .setPassword("afeiblog").setDatabase(0);
RedissonClient redissonClient3 = Redisson.create(config3);

String resourceName = "LOCK_KEY";

RLock lock1 = redissonClient1.getLock(resourceName);
RLock lock2 = redissonClient2.getLock(resourceName);
RLock lock3 = redissonClient3.getLock(resourceName);
// 向3个redis实例尝试加锁
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
boolean isLock;
try {
    // isLock = redLock.tryLock();
    // 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。
    isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);
    System.out.println("isLock = "+isLock);
    if (isLock) {
        //TODO if get lock success, do something;
    }
} catch (Exception e) {
} finally {
    // 无论如何, 最后都要解锁
    redLock.unlock();
}

这段源码有几个要点:

  • 首先需要构造N个RLock(源码中是3个,RLock就是普通的分布式锁)。
  • 然后用这N个RLock构造一个RedissonRedLock,这就是Redisson给我们封装好的RedLock分布式锁(即N个相互完全独立的节点)。
  • 调用unlock方法解锁,这个方法会向每一个RLock发起解锁请求(for (RLock lock : locks) {futures.add(lock.unlockAsync());})。
  • 这段源码我们是基于3个完全独立的Redis单机实例来实现的(config1.useSingleServer())。当然,我们也可以基于3个完全独立的主从(config.useMasterSlaveServers()),或者3个完全独立的sentinel集群(config.useSentinelServers()),或者3个完全独立的Cluster集群(config.useClusterServers().)。

假如我们依赖3个完全独立的Cluster集群来实现ReLock方案,那么架构图如下所示:
在这里插入图片描述

有些同学会反问,为什么需要3个Redis Cluster,一个行不行?回答这个问题之前,我们假设只有一个Redis Cluster,那么无论这个Cluster有多少个Master,我们是没办法让LOCK_KEY发送到多个Master上的,因为一个KEY只会属于Cluster中的一个Master,这一点也是没理解RedLock方案最容易犯的错误。

最后还有一个小小的注意点,Redis分布式锁加锁时value必须唯一,RedLock是怎么保证的呢?答案是UUID + threadId,源码如下:

protected final UUID id = UUID.randomUUID();
String getLockName(long threadId) {
    return id + ":" + threadId;
}

写在最后

RedLock方案相比普通的Redis分布式锁方案可靠性确实大大提升。但是,任何事情都具有两面性,因为我们的业务一般只需要一个Redis Cluster,或者一个Sentinel,但是这两者都不能承载RedLock的落地。如果你想要使用RedLock方案,还需要专门搭建一套环境。所以,如果不是对分布式锁可靠性有极高的要求(比如金融场景),不太建议使用RedLock方案。当然,作为基于Redis最牛的分布式锁方案,你依然必须掌握的非常好,以便在有需要时(比如面试)能应付自如。

在这里插入图片描述

<think>嗯,用户问的是如何使用Redis防止重复下单。这个问题看起来挺常见的,尤其是在电商或者高并发系统中。我得先理清楚用户的需求。重复下单可能是指同一个用户在短时间内多次提交相同的订单,这会导致库存问题或者重复支付等情况。所以,用户需要一种机制来防止这种情况发生。 首先,我需要回忆一下Redis的特性。Redis是内存数据库,读写速度快,支持原子操作,适合处理高并发场景。常见的防重方法可能有几种,比如使用唯一标识、分布式锁、Token机制或者幂等性处理。但用户这里可能更关注的是如何具体用Redis实现。 接下来,我得想一下具体步骤。假设用户在下单时,可能因为网络延迟或者用户多次点击导致同一请求被多次发送到服务器。这时候,服务器需要判断这个请求是否已经处理过。 可能的解决方案之一是生成一个唯一的请求标识,比如用户ID加商品ID加时间戳之类的组合,然后将这个标识存入Redis,并设置一定的过期时间。每次请求到来时,先检查Redis中是否存在这个标识,如果存在就拒绝处理,否则继续处理。但这种方法需要注意并发情况下的原子性问题,比如多个请求同时检查是否存在这个标识,可能会同时通过检查,导致重复下单。 这时候可能需要用到Redis的setnx命令(SET if Not eXists),或者使用带有过期时间的SET命令,并确保原子性。比如使用SET key value NX EX 过期时间,这样可以在设置键的同时设置过期时间,避免重复下单。这里的key可以是订单的唯一标识,比如用户ID+商品ID或者其他业务相关的唯一组合。 另外,还可以考虑使用分布式锁。比如,当用户开始下单时,先获取一个锁,锁住用户ID或者订单相关资源,处理完后再释放锁。这样可以防止同一个用户的并发请求。RedisRedLock算法或者简单的setnx都可以用来实现分布式锁,但需要注意锁的超时时间正确释放,避免死锁或者锁被其他进程误删。 还有一种方法是利用Redis的原子递增操作。例如,为每个用户商品生成一个key,每次下单前先检查这个key的值是否超过阈值,或者通过INCR命令来计数,如果计数超过1则拒绝。但这种方法可能不如设置唯一标识直接有效。 另外,幂等性处理也是一个思路。服务端在接收到请求时,先生成一个唯一的token返回给客户端,客户端下单时必须携带这个token。服务器在处理请求时,先检查token是否存在,存在则处理并删除token,否则拒绝。这种方法需要客户端配合,可能不适合所有场景。 不过,用户的问题更偏向于如何用Redis防止重复下单,所以可能最适合的是使用唯一key加过期时间,结合setnx命令。例如,当用户提交订单时,生成一个唯一的key,如order:user123:product456,然后尝试在Redis中设置这个key,如果成功(即之前不存在),则处理订单,并设置一个较短的过期时间(比如30秒)。如果设置失败,说明已经有相同的订单在处理中,直接返回重复提交的错误。 需要注意的是,这个key的生成规则必须确保唯一性,比如结合用户ID、商品ID、时间戳(比如分钟级,避免同一用户同一商品在一分钟内重复下单),或者使用更精确的业务字段组合。同时,过期时间的设置要根据业务需求来定,太短可能无法有效防止重复,太长可能影响用户体验。 可能还要考虑不同客户端的重复提交,比如用户在手机电脑同时提交,这时候可能需要更复杂的机制,但基本的防重复下单还是可以用上述方法。 总结来说,步骤大概是:生成唯一请求标识,用Redis的setnx或SET NX EX命令存入,处理订单前检查是否存在,存在则拒绝,否则继续处理,并设置合适的过期时间。同时要考虑并发情况下的原子性操作,确保即使多个请求同时到达,只有一个能成功设置key,从而防止重复下单。</think>使用 Redis 防止重复下单的核心思路是通过**唯一标识+原子性操作**拦截重复请求,以下是具体实现步骤注意事项: --- ### **1. 核心方案:基于唯一请求标识** #### **步骤说明** 1. **生成唯一标识** 将用户ID、商品ID、时间戳(如秒级/分钟级)等业务参数拼接,或通过哈希(如MD5)生成唯一标识,例如: `order_lock:user123:product456` 或 `order_lock:MD5(用户ID+商品ID+下单时间戳)`。 2. **Redis 写入校验** 使用 `SET key value NX EX seconds` 命令(原子性操作): - **NX**:仅当 Key 不存在时才能写入,避免并发覆盖。 - **EX**:设置自动过期时间(根据业务设定,如 10~30 秒)。 ```bash SET order_lock:user123:product456 1 NX EX 30 ``` 3. **校验结果处理** - 如果写入成功 → 允许下单,继续后续业务流程。 - 如果写入失败 → 直接返回 "请勿重复提交" 提示。 4. **过期时间结束后** Redis 自动删除 Key,允许用户再次正常下单。 --- ### **2. 进阶优化:防止极端并发场景** #### **场景问题** 若用户疯狂点击导致同一毫秒内大量并发请求,可能绕过时间戳拼接的唯一性。可补充以下方案: - **方案一:分布式锁(更严格)** 下单前对用户ID加锁(如 `lock:user123`),通过 Redis 的 `SETNX` 或 `Redisson` 库实现锁竞争,确保同一用户串行处理请求。 - **方案二:Token 机制(需前端配合)** 1. 前端下单前先请求服务端获取一次性 Token(存 Redis 并设置过期时间)。 2. 提交订单时携带 Token,服务端验证 Token 存在后删除,防止重复使用。 --- ### **3. 关键注意事项** 1. **原子性操作** 必须使用 `SET NX EX` 或 Lua 脚本确保“判断+写入”的原子性,避免高并发下多个请求同时通过校验。 2. **合理设置过期时间** 根据业务容忍度设置,例如: - 短时效(10~30秒):拦截短时间内的重复点击。 - 长时效(分钟级):防止恶意用户周期性重试。 3. **异常补偿机制** 若下单失败(如支付超时),需手动删除 Redis Key 或设置更短的过期时间,避免阻塞用户正常重试。 --- ### **4. 代码示例(Python)** ```python import redis import hashlib def generate_order_key(user_id, product_id): # 示例:生成唯一Key(可结合时间戳) return f"order_lock:{user_id}:{product_id}" def prevent_duplicate_order(user_id, product_id): r = redis.Redis(host='localhost', port=6379) key = generate_order_key(user_id, product_id) # 原子性写入,NX=True(不存在才写入),EX=30秒 success = r.set(key, "1", ex=30, nx=True) if not success: return {"code": 400, "msg": "检测到重复下单,请稍后再试"} else: # 正常处理订单逻辑... return {"code": 200, "msg": "下单成功"} ``` --- ### **总结** 通过 Redis 的原子性操作唯一Key机制,能高效拦截 99% 的重复下单场景。结合分布式锁或 Token 机制可进一步优化极端情况,最终根据业务需求平衡安全性与用户体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无休居士

感谢您的支持,我会继续努!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值