基于Java开发的Redis互斥锁写法(生产级规范)
基于Java开发的Redis互斥锁写法(生产级规范)
<!-- Maven依赖示例 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- application.yml配置示例 -->
spring:
redis:
host: localhost
port: 6379
password: your_password
database: 0
lettuce:
pool:
max-active: 16 # 连接池最大连接数
max-idle: 8 # 连接池最大空闲连接
min-idle: 4 # 连接池最小空闲连接
max-wait: 1000ms # 连接池最大等待时间 -->
一、基础版互斥锁(单节点Redis,简单并发场景)
1.1 核心逻辑
1.2 代码实现(Spring Boot + RedisTemplate)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* 基础版Redis互斥锁(单节点,适用于简单并发场景)
*/
@Component
public class RedisBasicMutexLock {
// 注入StringRedisTemplate(优先使用,避免序列化问题)
private final StringRedisTemplate stringRedisTemplate;
// 构造器注入(Spring 4.3+支持,无需@Autowired)
public RedisBasicMutexLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 获取互斥锁
* @param lockKey 锁的key(需区分业务资源,如"lock:product:1001")
* @param expireSeconds 锁的过期时间(秒),必须大于业务处理时间
* @return 锁的value(唯一标识,用于释放锁校验),null表示获取锁失败
*/
public String acquireLock(String lockKey, long expireSeconds) {
// 生成唯一value(UUID,避免误释放其他客户端的锁)
String lockValue = UUID.randomUUID().toString();
// 原子操作:SET NX EX,key不存在则设置,同时指定过期时间
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
// 若获取锁成功,返回唯一value;失败返回null
return Boolean.TRUE.equals(success) ? lockValue : null;
}
/**
* 释放互斥锁(校验锁归属,避免误删)
* @param lockKey 锁的key
* @param lockValue 获取锁时返回的唯一value
* @return true:释放成功;false:释放失败(锁不存在或归属不符)
*/
public boolean releaseLock(String lockKey, String lockValue) {
// 1. 获取当前锁的value
String currentValue = stringRedisTemplate.opsForValue().get(lockKey);
// 2. 校验:锁存在且value与当前客户端的value一致,才释放
if (lockValue != null && lockValue.equals(currentValue)) {
// 手动删除锁
stringRedisTemplate.delete(lockKey);
return true;
}
return false;
}
}
// 业务使用示例(Service层)
@Component
public class ProductService {
private final RedisBasicMutexLock basicMutexLock;
public ProductService(RedisBasicMutexLock basicMutexLock) {
this.basicMutexLock = basicMutexLock;
}
// 模拟:商品库存扣减(需要互斥锁避免并发超卖)
public boolean deductStock(Long productId) {
String lockKey = "lock:product:stock:" + productId;
String lockValue = null;
try {
// 尝试获取锁,过期时间设为10秒(业务处理预计5秒)
lockValue = basicMutexLock.acquireLock(lockKey, 10);
if (lockValue == null) {
// 获取锁失败,返回失败(或重试)
System.out.println("获取锁失败,当前有其他线程正在处理商品库存");
return false;
}
// 执行业务逻辑:查询库存、扣减库存(模拟耗时操作)
System.out.println("获取锁成功,开始扣减商品" + productId + "库存");
Thread.sleep(5000); // 模拟业务耗时
System.out.println("商品库存扣减完成");
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("业务处理异常");
return false;
} finally {
// 无论业务是否成功,都释放锁(避免死锁)
if (lockValue != null) {
basicMutexLock.releaseLock(lockKey, lockValue);
System.out.println("锁已释放");
}
}
}
}
1.3 核心注意事项
- 锁key命名规范:必须携带业务标识和资源ID(如“lock:product:stock:1001”),避免不同业务、不同资源共用一个key,导致锁冲突。
- lockValue必须唯一:使用UUID生成,核心目的是避免“误释放锁”——若不校验value,客户端A获取的锁,可能被客户端B误删除(如A处理超时,锁未过期前B获取锁,A处理完后删除B的锁)。
- 过期时间设置:必须大于业务处理的最大耗时(建议预留2-3倍冗余,如业务耗时5秒,过期时间设10-15秒),避免锁提前过期导致并发冲突。
- finally块释放锁:无论业务是否正常执行、是否抛出异常,都必须在finally块中释放锁,防止客户端宕机或业务异常导致锁无法释放,引发死锁。
- 适用场景:单节点Redis、并发量适中、业务处理时间固定的简单场景(如简单的库存扣减、缓存更新)。
二、进阶版互斥锁(带锁续期,解决业务耗时不确定问题)
2.1 核心优化点
2.2 代码实现(带续期机制)
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 进阶版Redis互斥锁(带锁续期,解决业务耗时不确定问题)
*/
@Component
public class RedisRenewMutexLock {
private final StringRedisTemplate stringRedisTemplate;
public RedisRenewMutexLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 获取锁并启动续期线程
* @param lockKey 锁的key
* @param expireSeconds 锁的初始过期时间(秒)
* @return 锁信息(包含lockKey、lockValue、续期开关),null表示获取锁失败
*/
public LockInfo acquireLock(String lockKey, long expireSeconds) {
String lockValue = UUID.randomUUID().toString();
// 原子操作获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireSeconds, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(success)) {
return null;
}
// 初始化锁信息,设置续期开关(原子布尔值,保证线程安全)
LockInfo lockInfo = new LockInfo(lockKey, lockValue, expireSeconds);
// 启动续期线程(守护线程,主线程结束后自动退出)
startRenewThread(lockInfo);
return lockInfo;
}
/**
* 启动续期线程
* 续期间隔:过期时间的1/3(如过期10秒,每3秒续期一次)
*/
private void startRenewThread(LockInfo lockInfo) {
Thread renewThread = new Thread(() -> {
while (!lockInfo.isStopRenew()) {
try {
// 每隔过期时间的1/3秒,执行一次续期
Thread.sleep(lockInfo.getExpireSeconds() * 1000 / 3);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
// 校验锁是否归当前客户端所有,若是则续期
String currentValue = stringRedisTemplate.opsForValue().get(lockInfo.getLockKey());
if (lockInfo.getLockValue().equals(currentValue)) {
// 续期:重置锁的过期时间
stringRedisTemplate.expire(lockInfo.getLockKey(), lockInfo.getExpireSeconds(), TimeUnit.SECONDS);
System.out.println("锁续期成功,key:" + lockInfo.getLockKey() + ",新过期时间:" + lockInfo.getExpireSeconds() + "秒");
} else {
// 锁已失效或归属其他客户端,停止续期
lockInfo.setStopRenew(true);
}
}
});
// 设为守护线程,避免主线程结束后,续期线程仍占用资源
renewThread.setDaemon(true);
renewThread.start();
}
/**
* 释放锁(停止续期,校验归属后删除)
* @param lockInfo 获取锁时返回的锁信息
* @return true:释放成功;false:释放失败
*/
public boolean releaseLock(LockInfo lockInfo) {
if (lockInfo == null) {
return false;
}
// 1. 停止续期线程
lockInfo.setStopRenew(true);
// 2. 校验锁归属
String currentValue = stringRedisTemplate.opsForValue().get(lockInfo.getLockKey());
if (lockInfo.getLockValue().equals(currentValue)) {
// 3. 删除锁
stringRedisTemplate.delete(lockInfo.getLockKey());
System.out.println("锁释放成功,key:" + lockInfo.getLockKey());
return true;
}
System.out.println("锁已失效或归属其他客户端,无需释放,key:" + lockInfo.getLockKey());
return false;
}
/**
* 锁信息封装(存储锁的核心信息,线程安全)
*/
public static class LockInfo {
private final String lockKey;
private final String lockValue;
private final long expireSeconds;
// 续期开关(原子布尔值,保证多线程操作安全)
private final AtomicBoolean stopRenew = new AtomicBoolean(false);
public LockInfo(String lockKey, String lockValue, long expireSeconds) {
this.lockKey = lockKey;
this.lockValue = lockValue;
this.expireSeconds = expireSeconds;
}
// getter/setter
public String getLockKey() { return lockKey; }
public String getLockValue() { return lockValue; }
public long getExpireSeconds() { return expireSeconds; }
public boolean isStopRenew() { return stopRenew.get(); }
public void setStopRenew(boolean stop) { stopRenew.set(stop); }
}
}
// 业务使用示例
@Component
public class OrderService {
private final RedisRenewMutexLock renewMutexLock;
public OrderService(RedisRenewMutexLock renewMutexLock) {
this.renewMutexLock = renewMutexLock;
}
// 模拟:订单创建(业务耗时不确定,需锁续期)
public boolean createOrder(Long orderId) {
String lockKey = "lock:order:create:" + orderId;
RedisRenewMutexLock.LockInfo lockInfo = null;
try {
// 获取锁,初始过期时间10秒(业务耗时可能超过10秒)
lockInfo = renewMutexLock.acquireLock(lockKey, 10);
if (lockInfo == null) {
System.out.println("获取锁失败,当前有其他线程正在创建订单" + orderId);
return false;
}
// 模拟业务耗时(超过初始过期时间)
System.out.println("获取锁成功,开始创建订单" + orderId);
Thread.sleep(15000); // 耗时15秒,续期机制会生效
System.out.println("订单" + orderId + "创建完成");
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("订单创建异常");
return false;
} finally {
// 释放锁(自动停止续期)
renewMutexLock.releaseLock(lockInfo);
}
}
}
2.3 核心注意事项
- 续期线程设计:采用守护线程,避免主线程结束后,续期线程仍在运行,浪费系统资源;续期间隔设为“过期时间的1/3”,既避免频繁续期,又能确保锁不会提前过期。
- 线程安全:使用AtomicBoolean作为续期开关,保证多线程环境下续期开关的操作安全,避免出现“续期线程无法停止”的问题。
- 锁信息封装:将lockKey、lockValue、过期时间、续期开关封装为LockInfo类,便于管理和传递,提升代码可读性和可维护性。
- 异常处理:续期线程中捕获InterruptedException,避免线程异常挂起;业务线程中也需处理中断异常,确保程序优雅退出。
- 适用场景:单节点Redis、业务处理时间不确定(如复杂的订单处理、数据同步)、并发量中等的场景。
三、分布式互斥锁(Redis Cluster/主从架构,生产推荐)
3.1 核心痛点解决
3.2 代码实现(Redisson + Spring Boot)
<!-- Maven依赖(Redisson) -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version> <!-- 版本与Spring Boot版本适配,可自行调整 -->
</dependency>
<!-- application.yml配置(Redisson集群配置,适配Redis Cluster) -->
spring:
redis:
cluster:
nodes:
- localhost:6379
- localhost:6380
- localhost:6381
- localhost:6382
- localhost:6383
- localhost:6384
password: your_password
# Redisson配置(可选,默认配置可满足大部分场景)
redisson:
lock:
watchdog-timeout: 30000 # 看门狗续期超时时间(默认30秒)
threads: 16 # 处理线程数
netty-threads: 32 # netty线程数
// 分布式锁实现(基于Redisson)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 分布式Redis互斥锁(基于Redisson,适配Redis Cluster/主从架构,生产级)
*/
@Component
public class RedisDistributedMutexLock {
// 注入RedissonClient(Redisson自动配置,无需手动创建)
private final RedissonClient redissonClient;
public RedisDistributedMutexLock(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 获取分布式锁(自动续期,默认看门狗机制)
* @param lockKey 锁的key(区分业务资源)
* @param waitTime 获取锁的等待时间(秒):获取不到锁时,最多等待多久
* @param leaseTime 锁的租赁时间(秒):若为-1,启用看门狗自动续期(默认30秒,可配置)
* @return RLock:锁对象,用于释放锁;null:获取锁失败
*/
public RLock acquireLock(String lockKey, long waitTime, long leaseTime) {
// 获取分布式锁(公平锁/非公平锁,默认非公平锁;可指定为公平锁:redissonClient.getFairLock(lockKey))
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁:waitTime等待时间,leaseTime租赁时间,时间单位秒
boolean success = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
return success ? lock : null;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
/**
* 释放分布式锁
* @param lock 获取锁时返回的RLock对象
*/
public void releaseLock(RLock lock) {
if (lock != null && lock.isHeldByCurrentThread()) {
// 仅释放当前线程持有的锁,避免误释放其他线程的锁
lock.unlock();
System.out.println("分布式锁释放成功,key:" + lock.getName());
}
}
}
// 业务使用示例(分布式场景:跨服务商品库存扣减)
@Component
public class DistributedProductService {
private final RedisDistributedMutexLock distributedMutexLock;
public DistributedProductService(RedisDistributedMutexLock distributedMutexLock) {
this.distributedMutexLock = distributedMutexLock;
}
public boolean deductDistributedStock(Long productId) {
String lockKey = "lock:distributed:product:stock:" + productId;
RLock lock = null;
try {
// 获取锁:等待时间3秒(获取不到锁则超时),租赁时间-1(启用看门狗自动续期)
lock = distributedMutexLock.acquireLock(lockKey, 3, -1);
if (lock == null) {
System.out.println("获取分布式锁失败,当前有其他服务正在处理商品" + productId + "库存");
return false;
}
// 执行业务逻辑:跨服务查询库存、扣减库存(模拟分布式场景)
System.out.println("获取分布式锁成功,开始跨服务扣减商品" + productId + "库存");
Thread.sleep(8000); // 模拟业务耗时
System.out.println("商品" + productId + "库存扣减完成(分布式场景)");
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("分布式库存扣减异常");
return false;
} finally {
// 释放锁
distributedMutexLock.releaseLock(lock);
}
}
}
3.3 核心说明
- Redisson优势:封装了Redlock算法,自动处理多节点锁的获取、续期、释放,无需手动实现复杂逻辑;支持公平锁、非公平锁、可重入锁、读写锁等多种锁类型,适配不同分布式场景。
- 看门狗机制:当leaseTime设为-1时,Redisson会启动看门狗线程,每隔10秒(默认)自动续期锁的过期时间(默认续期为30秒),确保业务未处理完成时,锁不会过期;业务处理完成后,手动释放锁,看门狗自动停止。
- 集群要求:Redis集群需为Cluster架构(至少3个主节点),或主从+哨兵架构,确保单个节点宕机后,锁仍能正常工作(超过半数节点持有锁,即可保证锁有效)。
- 锁的释放:必须使用获取锁时返回的RLock对象释放,Redisson会自动校验锁的归属,避免误释放;同时,isHeldByCurrentThread()方法可判断当前线程是否持有锁,进一步提升安全性。
3.4 核心注意事项
- Redisson版本适配:需根据Spring Boot版本选择对应的Redisson版本,避免版本冲突(如Spring Boot 2.7.x,推荐Redisson 3.23.x)。
- 等待时间(waitTime)设置:根据业务并发量合理设置,避免等待时间过长导致请求超时,或过短导致获取锁失败率过高(如并发量高,可设3-5秒)。
- 租赁时间(leaseTime)设置:若业务处理时间固定,可设置具体的租赁时间(大于业务耗时);若时间不确定,设为-1,启用看门狗自动续期(推荐)。
- 避免锁滥用:分布式锁会降低系统并发性能,仅在需要解决跨服务、跨节点并发冲突的场景使用(如分布式库存扣减、分布式订单创建)。
- 容错性:Redisson支持失败重试机制,当某个Redis节点宕机时,会自动尝试其他节点获取锁,提升分布式锁的可用性。
四、Redis读数据库时的双重锁Java代码方案
4.1 核心场景与设计思路
- 第一重锁(Redis分布式锁):拦截跨线程、跨服务的并发请求,确保同一时刻只有一个请求能进入“读数据库+缓存更新”流程,避免分布式场景下的并发穿透。
- 第二重锁(本地锁,synchronized):拦截同一进程内的多线程并发请求,减少Redis锁的竞争频率,提升性能(避免同一进程内多个线程频繁请求Redis获取锁)。
4.2 生产级代码实现(Spring Boot + Redisson)
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.concurrent.TimeUnit;
/**
* Redis读数据库时的双重锁实现(生产级,适配分布式场景)
* 双重锁:本地锁(synchronized)+ Redis分布式锁
*/
@Component
public class RedisDoubleLockReadService {
// 注入Redis和Redisson客户端
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;
// 本地锁对象(全局唯一,确保同一进程内锁的唯一性)
private final Object localLock = new Object();
// 构造器注入
public RedisDoubleLockReadService(StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient) {
this.stringRedisTemplate = stringRedisTemplate;
this.redissonClient = redissonClient;
}
/**
* 双重锁读取数据(先查Redis,未命中则读库并缓存)
* @param cacheKey Redis缓存key(如"cache:product:1001")
* @param lockKey Redis分布式锁key(如"lock:read:product:1001")
* @param productId 业务资源ID(如商品ID)
* @return 读取到的数据(String类型,可根据业务封装为实体类)
*/
public String readDataWithDoubleLock(String cacheKey, String lockKey, Long productId) {
// 1. 第一重校验:查询Redis缓存,命中则直接返回
String cacheData = stringRedisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(cacheData)) {
System.out.println("Redis缓存命中,直接返回数据");
return cacheData;
}
// 2. 第二重校验:加本地锁(synchronized),拦截同一进程内多线程并发
synchronized (localLock) {
// 2.1 再次查询Redis(避免本地其他线程已缓存数据,减少Redis锁竞争)
cacheData = stringRedisTemplate.opsForValue().get(cacheKey);
if (StringUtils.hasText(cacheData)) {
System.out.println("本地锁内Redis缓存命中,返回数据");
return cacheData;
}
// 3. 第三重:加Redis分布式锁,拦截跨进程/跨服务并发
RLock redisLock = redissonClient.getLock(lockKey);
try {
// 尝试获取Redis锁:等待3秒,租赁时间10秒(启用看门狗自动续期)
boolean lockSuccess = redisLock.tryLock(3, -1, TimeUnit.SECONDS);
if (!lockSuccess) {
// 获取锁失败,返回默认值或提示,避免阻塞
System.out.println("获取Redis分布式锁失败,无法读取数据库");
return "暂时无法获取数据,请稍后重试";
}
// 4. 锁获取成功,读取数据库(模拟业务逻辑)
System.out.println("Redis锁获取成功,开始读取数据库数据");
String dbData = readDataFromDb(productId); // 实际场景中调用DAO层方法
// 5. 读取数据库成功后,更新Redis缓存(设置过期时间,避免缓存雪崩)
if (StringUtils.hasText(dbData)) {
stringRedisTemplate.opsForValue().set(cacheKey, dbData, 300, TimeUnit.SECONDS);
System.out.println("数据库数据读取成功,已更新Redis缓存");
}
// 6. 返回数据库数据
return dbData;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("双重锁读取数据异常");
return null;
} finally {
// 释放Redis分布式锁(仅当前线程持有锁时释放)
if (redisLock != null && redisLock.isHeldByCurrentThread()) {
redisLock.unlock();
System.out.println("Redis分布式锁已释放");
}
}
}
}
/**
* 模拟从数据库读取数据(实际场景中替换为DAO层查询)
* @param productId 商品ID
* @return 数据库中的商品数据
*/
private String readDataFromDb(Long productId) {
// 模拟数据库查询耗时
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 模拟返回数据库数据(实际场景中封装为实体类,此处用String简化)
return "商品ID:" + productId + ",商品名称:测试商品,库存:100";
}
}
// 业务使用示例(Controller/Service层)
@Component
public class ProductReadService {
private final RedisDoubleLockReadService doubleLockReadService;
public ProductReadService(RedisDoubleLockReadService doubleLockReadService) {
this.doubleLockReadService = doubleLockReadService;
}
// 读取商品详情(双重锁保护,避免缓存穿透)
public String getProductDetail(Long productId) {
// 定义缓存key和锁key(遵循命名规范)
String cacheKey = "cache:product:detail:" + productId;
String lockKey = "lock:read:product:detail:" + productId;
// 调用双重锁读取方法
return doubleLockReadService.readDataWithDoubleLock(cacheKey, lockKey, productId);
}
}
4.3 核心注意事项
- 本地锁设计:使用synchronized锁(锁全局唯一对象localLock),确保同一进程内所有线程竞争同一把锁,避免同一进程内多线程频繁请求Redis锁,提升性能。
- 双重校验缓存:两次查询Redis(锁前、本地锁内),避免“缓存刚被其他线程更新”导致的无效数据库查询,进一步减少穿透。
- Redis锁选型:使用Redisson分布式锁,支持自动续期、锁归属校验,避免死锁和误释放,适配分布式集群场景;若为单节点Redis,可替换为基础版Redis锁。
- 锁粒度控制:锁key需绑定具体业务资源(如“lock:read:product:detail:1001”),避免全局锁,提升并发能力。
- 异常处理:捕获InterruptedException,确保线程中断后能优雅退出;释放Redis锁时,必须校验锁归属,避免误释放其他线程的锁。
- 缓存更新:读取数据库后,必须设置Redis缓存过期时间(如300秒),避免缓存雪崩;同时可结合业务场景,设置合理的过期时间冗余。
- 适用场景:高并发读场景、缓存穿透/击穿风险较高的场景(如商品详情查询、用户信息查询),兼顾性能与数据一致性。
4.4 流程展示理解
下图借鉴:https://blog.csdn.net/weixin_63434398/article/details/156640252
请求处理流程
用户请求
│
▼
┌───────────────────────┐
│ 构建 fullShortUrl │
└───────────────────────┘
│
┌─────────────────────────┴─────────────────────────┐
│ 无锁区域 │
│ ┌─────────────┐ 命中 ┌──────────────┐ │
│ │ 查缓存 │────────────▶│ 直接跳转 │ │
│ └─────────────┘ └──────────────┘ │
│ │ 未命中 │
│ ▼ │
│ ┌─────────────┐ 不存在 ┌──────────────┐ │
│ │ 布隆过滤器 │────────────▶│ 返回 404 │ │
│ └─────────────┘ └──────────────┘ │
│ │ 可能存在 │
│ ▼ │
│ ┌─────────────┐ 存在 ┌──────────────┐ │
│ │ 空值缓存 │────────────▶│ 返回 404 │ │
│ └─────────────┘ └──────────────┘ │
│ │ 不存在 │
└────────┴──────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ 获取分布式锁 │
│ lock.lock() │
└───────────────────────┘
│
┌────────┴──────────────────────────────────────────┐
│ 有锁区域 │
│ ┌─────────────┐ 命中 ┌──────────────┐ │
│ │ 再查缓存 │────────────▶│ 直接跳转 │ │
│ │ (双重判定) │ │ (别人加载的) │ │
│ └─────────────┘ └──────────────┘ │
│ │ 仍未命中 │
│ ▼ │
│ ┌─────────────┐ 存在 ┌──────────────┐ │
│ │ 再查空值 │────────────▶│ 返回 404 │ │
│ │ (双重判定) │ └──────────────┘ │
│ └─────────────┘ │
│ │ 仍不存在 │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ 查询数据库 │ │
│ │ 路由表 → 短链接表 → 写入缓存 → 跳转 │ │
│ └─────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
│
▼
┌───────────────────────┐
│ 释放锁 │
│ lock.unlock() │
└───────────────────────┘
并发场景时序图
假设三个请求几乎同时到来,缓存为空
时间轴 ──────────────────────────────────────────────────────▶
请求A ─────┬──────────────────────────────────────────────────
│ 查缓存 → 未命中
│ 查布隆 → 可能存在
│ 查空值 → 不存在
│ 获取锁 ✓
│ 双重判定 → 仍未命中
│ 查数据库...
│ 写入缓存 ◀──────────────── 这时候缓存有值了
│ 释放锁
└──▶ 跳转成功
请求B ───────────┬────────────────────────────────────────────
│ 查缓存 → 未命中
│ 查布隆 → 可能存在
│ 查空值 → 不存在
│ 等待锁... ⏳
│ │
│ ▼ (A释放锁后)
│ 获取锁 ✓
│ 双重判定 → 命中!(A已写入)
│ 释放锁
└──▶ 直接跳转,没查库!
请求C ───────────────────────────────────────────────────┬────
│ 查缓存 → 命中!
└──▶ 直接跳转,没加锁!
五、生产级通用规范(所有写法必遵循)
- 连接池配置:必须配置Redis连接池(lettuce或jedis),设置合理的最大连接数、空闲连接数、等待时间,避免频繁创建、关闭连接,提升性能和稳定性。
- 锁key命名规范:统一格式为“lock:业务模块:资源标识”(如“lock:product:stock:1001”“lock:order:create:2024”),确保锁的唯一性,避免冲突。
- 异常处理:所有锁的操作(获取、释放、续期)都需捕获异常(如Redis连接异常、中断异常),避免程序崩溃,同时记录日志,便于故障排查。
- 锁的粒度:尽量缩小锁的粒度,避免使用全局锁(如“lock:product:all”),改为细粒度锁(如“lock:product:stock:1001”),提升系统并发能力。
- 日志记录:在获取锁、释放锁、续期、获取锁失败等关键节点,记录详细日志(如锁key、客户端标识、操作时间),便于排查并发问题。
- 避免长时间持有锁:业务逻辑尽量简洁,减少锁的持有时间(如将非核心逻辑移出锁的范围),避免影响系统并发性能。
- 压测验证:上线前,对互斥锁进行压测,模拟高并发场景,验证锁的有效性、性能和稳定性,避免出现死锁、并发冲突等问题。

浙公网安备 33010602011771号