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

Redis 实现分布式锁的探索与实践

一、问题的产生:秒杀功能中的超卖现象

在开发秒杀功能时,最初的逻辑很简单:判断商品库存是否大于 0,若大于则扣减库存,否则秒杀失败。然而上线后,出现了库存只有 1 个,却卖出多份的超卖问题。

这是因为在多线程并发场景下,多个线程同时对共享的库存资源进行读写,会导致数据错乱。

private static int stock = 1; // 假设初始库存为1public static void placeOrder() throws Exception {if (stock > 0) {Thread.sleep(100);stock--;System.out.println(Thread.currentThread().getName() + "秒杀成功");} else {System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");}
}public static void main(String[] args) throws Exception {for (int i = 0; i < 3; i++) {new Thread(() -> {try {placeOrder();} catch (Exception e) {e.printStackTrace();}}).start();}
}

运行结果:

二、初步尝试:使用synchronized锁

为解决多线程并发问题,我们使用了 synchronized 同步锁对秒杀逻辑进行改造。改造后进行压测,超卖问题确实得到解决。

private static final Object lock = new Object();private static int stock = 1;public static void placeOrder() throws Exception {synchronized (lock) {if (stock > 0) {Thread.sleep(100);stock--;System.out.println(Thread.currentThread().getName() + "秒杀成功");} else {System.out.println(Thread.currentThread().getName() + "秒杀失败!库存不足");}}}public static void main(String[] args) throws Exception {for (int i = 0; i < 3; i++) {new Thread(() -> {try {placeOrder();} catch (Exception e) {e.printStackTrace();}}).start();}}

运行结果:

但随着用户量增长,服务器压力增大、性能达到瓶颈。于是我们采用 Nginx 负载均衡进行服务器水平扩展,构建分布式集群。可压测时发现,秒杀功能又出现超卖问题。

这是因为 synchronized是 JVM 级别的锁,只能锁住单个进程内的线程。在分布式部署后,每台服务器的 synchronized 锁只能控制自身服务器内的线程,无法跨服务器协调,多个服务器的线程仍会并发操作库存,导致超卖。

三、分布式锁的引入:Redis方案

为解决分布式场景下的并发问题,我们引入分布式锁,主流的分布式锁实现有 Redis 和 ZooKeeper,这里选择 Redis 来实现。

3.1 Redis分布式锁的核心原理(基于SETNX)

Redis 的 SETNX(Set If Not Exists)命令是实现分布式锁的关键。当一个线程向 Redis 中通过 SETNX 存储一个键值对时:

  • 如果该键不存在,就存储成功并返回 True,表示获取到锁。
  • 如果该键已存在,存储失败并返回 False,表示获取锁失败。

利用这个特性,我们可以让多个服务器上的线程,通过争抢 Redis 中的 “锁键”,来实现对秒杀资源的互斥访问。

3.2 Redis分布式锁的关键要点

1. 必须设置锁的过期时间
如果不设置过期时间,当持有锁的线程意外挂掉(如服务器宕机),锁会一直存在,其他线程会一直等待,陷入死锁。

2. 处理业务超时问题
若业务处理时间超过锁的过期时间,锁会自动释放,其他线程就会抢占锁,可能导致业务逻辑混乱。
解决方法有两种:

  • 延长锁时间 + 心跳机制:加长锁的过期时间,并启动一个子线程,每 10 秒检查持有锁的线程是否在线,若在线则重置锁的过期时间。
  • 给锁添加唯一标识:为每把锁设置唯一 ID(如 UUID),确保锁的 key 与持有它的线程绑定,防止线程释放其他线程的锁。

3.3 Redis的特性与red lock

Redis 采用 AP 模型,追求高可用和高性能,但不保证强一致性。
red lock 则致力于保证一致性,它要求所有参与的 Redis 节点(主从复制架构中,主节点和从节点都保存成功)都成功保存锁信息,才会返回加锁成功,以此提高分布式锁的可靠性。

四、Redis分布式锁在Java中的实现

在Java中使用Redis实现分布式锁有多种方式,下面我将介绍几种常见的实现方案及其代码示例。

方案一:基于SETNX命令的基础实现

1. 添加Redis依赖

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>4.4.3</version>
</dependency>

2. 基础实现代码

import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;import java.util.Collections;public class RedisDistributedLock {private static final String LOCK_SUCCESS = "OK";private static final String SET_IF_NOT_EXIST = "NX";private static final String SET_WITH_EXPIRE_TIME = "PX";private static final Long RELEASE_SUCCESS = 1L;private Jedis jedis;public RedisDistributedLock(Jedis jedis) {this.jedis = jedis;}/*** 尝试获取分布式锁* @param lockKey 锁的key* @param requestId 请求标识(用于标识锁的持有者)* @param expireTime 超期时间(毫秒)* @return 是否获取成功*/public boolean tryLock(String lockKey, String requestId, int expireTime) {SetParams params = SetParams.setParams().nx()  // NX: 仅当key不存在时设置.px(expireTime);  // PX: 设置过期时间(毫秒)String result = jedis.set(lockKey, requestId, params);return LOCK_SUCCESS.equals(result);}/*** 释放分布式锁* @param lockKey 锁的key* @param requestId 请求标识* @return 是否释放成功*/public boolean releaseLock(String lockKey, String requestId) {String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) " +"else " +"return 0 " +"end";Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));return RELEASE_SUCCESS.equals(result);}/*** 尝试获取锁(带重试机制)*/public boolean lockWithRetry(String lockKey, String requestId, int expireTime, int retryTimes, long sleepMillis) {for (int i = 0; i < retryTimes; i++) {if (tryLock(lockKey, requestId, expireTime)) {return true;}try {Thread.sleep(sleepMillis);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}return false;}
}

方案二:使用Redisson框架(推荐)

1. 添加Redisson依赖

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.23.2</version>
</dependency>

2. Redisson实现代码

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;import java.util.concurrent.TimeUnit;public class RedissonDistributedLock {private RedissonClient redissonClient;public RedissonDistributedLock() {Config config = new Config();config.useSingleServer().setAddress("redis://127.0.0.1:6379");this.redissonClient = Redisson.create(config);}/*** 获取锁*/public boolean tryLock(String lockKey, long waitTime, long leaseTime, TimeUnit unit) {RLock lock = redissonClient.getLock(lockKey);try {return lock.tryLock(waitTime, leaseTime, unit);} catch (InterruptedException e) {Thread.currentThread().interrupt();return false;}}/*** 释放锁*/public void unlock(String lockKey) {RLock lock = redissonClient.getLock(lockKey);if (lock.isLocked() && lock.isHeldByCurrentThread()) {lock.unlock();}}/*** 关闭Redisson客户端*/public void shutdown() {if (redissonClient != null) {redissonClient.shutdown();}}
}

3. 使用示例

public class LockExample {public static void main(String[] args) {RedissonDistributedLock lockService = new RedissonDistributedLock();String lockKey = "order:lock:1001";try {// 尝试获取锁,最多等待10秒,锁持有时间30秒boolean acquired = lockService.tryLock(lockKey, 10, 30, TimeUnit.SECONDS);if (acquired) {try {// 执行业务逻辑processOrder();} finally {// 释放锁lockService.unlock(lockKey);}} else {System.out.println("获取锁失败");}} finally {lockService.shutdown();}}private static void processOrder() {// 业务处理逻辑System.out.println("处理订单业务...");}
}

五、总结

从最初的单线程并发问题,到分布式场景下的并发控制,我们逐步探索出基于 Redis 的分布式锁方案来实现秒杀功能。Redis 分布式锁借助 SETNX 命令,结合过期时间、心跳机制等优化手段,能有效解决分布式秒杀中的超卖问题,同时在高可用、高性能方面也能满足秒杀场景的需求,当然 red lock 还能进一步提升锁的一致性保障。


文章转载自:

http://nKhFlhsm.nxbsq.cn
http://uoRd2tNj.nxbsq.cn
http://a3ynkr1G.nxbsq.cn
http://4TYYDyJ4.nxbsq.cn
http://r0MRQelD.nxbsq.cn
http://ok4OCrEm.nxbsq.cn
http://tVCESgmZ.nxbsq.cn
http://TQed90S9.nxbsq.cn
http://I7Vrai1H.nxbsq.cn
http://BMSBOjT2.nxbsq.cn
http://jjtksKDu.nxbsq.cn
http://g3Wlpuzi.nxbsq.cn
http://4wGfdd50.nxbsq.cn
http://HuLF28Dy.nxbsq.cn
http://OwjifaFD.nxbsq.cn
http://4ORf8Cz5.nxbsq.cn
http://CWmROA5q.nxbsq.cn
http://qGVRXARL.nxbsq.cn
http://L8FwWNk4.nxbsq.cn
http://l8maUCGC.nxbsq.cn
http://m1Lhz3Zd.nxbsq.cn
http://OBlqLE6I.nxbsq.cn
http://i9NCR17s.nxbsq.cn
http://I0VJGUqh.nxbsq.cn
http://rYOKNOMX.nxbsq.cn
http://LFxBojdO.nxbsq.cn
http://iYLpFrZ3.nxbsq.cn
http://8JlFpj1I.nxbsq.cn
http://WBdv1Zcd.nxbsq.cn
http://SOOBjaZ8.nxbsq.cn
http://www.dtcms.com/a/383235.html

相关文章:

  • 设计模式-适配器模式详解
  • Java 分布式缓存实现:结合 RMI 与本地文件缓存
  • Ajax-day2(图书管理)-渲染列表
  • 在Excel和WPS表格中快速复制上一行内容
  • 11-复习java程序设计中学习的面向对象编程
  • 《云计算如何驱动企业数字化转型:关键技术与实践案例》
  • LSTM 深度解析:从门控机制到实际应用
  • FPGA学习篇——Verilog学习Led灯的实现
  • 【ARDUINO】Arduino Uno 获取 OV7576 数据并通过 ESP8266 发送到 TCP 客户端(待测试)
  • xtuoj 原根
  • JVM 核心知识全解析:从类加载到垃圾回收的深度认知
  • Cesium4--地形(OSGB到3DTiles)
  • NLP:Transformer之self-attention(特别分享3)
  • 07 常用损失函数
  • UDP Socket 进阶:从 Echo 到字典服务器,学会 “解耦” 网络与业务
  • 多语言编码Agent解决方案(4)-Eclipse插件实现
  • 深入理解线程模型
  • LMCache:KV缓存管理
  • 关于物联网的基础知识(三)——物联网技术架构:连接万物的智慧之道!连接未来的万物之网!
  • 《嵌入式硬件(十一):基于IMX6ULL的中断操作》
  • 【Pywinauto库】12.4 pywinauto.uia_element_info后端内部实施模块
  • 工程机械健康管理物联网系统:移动互联与多工况诊断的技术实现
  • python递归解压压缩文件方法
  • 深入 Spring MVC 返回值处理器
  • 黑马JavaWeb+AI笔记 Day05 Web后端基础(JDBC)
  • Open3D 射线投射(Ray Casting,Python)
  • RL【10-1】:Actor - Critic
  • 计算机视觉(opencv)实战二十一——基于 SIFT 和 FLANN 的指纹图像匹配与认证
  • 纯`css`固定标题并在滚动时为其添加动画
  • 金融科技:银行中的风险管理