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 还能进一步提升锁的一致性保障。