手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁
mhr18 2025-08-06 21:37 3 浏览 0 评论
为什么要自己实现分布式锁?
你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成 Redisson 或者 Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半天。
其实在很多中小型项目里,分布式场景没那么复杂,用那些成熟框架有点 “杀鸡用牛刀” 的意思。自己实现的话,不仅能精准控制锁的逻辑,还能避开开源工具里那些用不上的冗余功能,让代码更轻量。就像上周有个读者留言说,他们项目里 Redis 版本比较老,用 Redisson 总出问题,最后还是靠自己写的简易锁解决了问题。
分布式锁的核心原则
在动手之前,咱们得先明确分布式锁必须满足的几个核心原则,这是保证锁可靠的基础:
- 互斥性:同一时间只能有一个线程拿到锁,这是分布式锁的基本要求。
- 防死锁:就算持有锁的线程意外挂了,锁也得能自动释放,避免永远阻塞其他线程。
- 容错性:万一 Redis 节点出点小问题,锁的逻辑不能跟着崩掉,要能尽量保证服务可用。
核心实现步骤(纯编码版)
准备工作:配置 Redis 环境
因为咱们要基于 Redis 实现分布式锁,所以先得在 Spring Boot3 里配置好 Redis 连接。
配置 RedisTemplate
新建 Redis 配置类,注入 StringRedisTemplate,记得设置序列化方式:
@Configuration
public class RedisConfig {
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 设置key和value的序列化方式(避免乱码)
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
}
配置文件信息
在 application.yml 里填写 Redis 连接参数:
spring:
redis:
host: localhost
port: 6379
timeout: 2000ms
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 2 # 最小空闲连接
定义锁接口:明确核心功能
先抽象出分布式锁的基本操作,方便后续实现和扩展:
public interface DistributedLock {
/**
* 立即获取锁(不等待)
* @param lockKey 锁的key
* @param expireTime 过期时间(防止死锁)
* @param timeUnit 时间单位
* @return 锁的标识(UUID),获取失败返回null
*/
String acquireLock(String lockKey, long expireTime, TimeUnit timeUnit);
/**
* 释放锁
* @param lockKey 锁的key
* @param lockValue 锁的标识(获取锁时返回的UUID)
* @return 是否释放成功
*/
boolean releaseLock(String lockKey, String lockValue);
/**
* 尝试获取锁(带等待时间)
* @param lockKey 锁的key
* @param waitTime 最大等待时间
* @param expireTime 锁过期时间
* @param timeUnit 时间单位
* @return 锁的标识,超时未获取返回null
* @throws InterruptedException 线程中断异常
*/
String tryAcquireLock(String lockKey, long waitTime, long expireTime, TimeUnit timeUnit) throws InterruptedException;
}
实现 Redis 锁:核心逻辑编码
这部分是关键,咱们用 Redis 原生 API 实现锁的获取和释放,全程不依赖第三方工具。
核心实现类
@Component
public class RedisDistributedLock implements DistributedLock {
private final StringRedisTemplate stringRedisTemplate;
// 释放锁的Lua脚本(保证判断和删除的原子性)
private static final String RELEASE_LOCK_LUA_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
// 构造器注入Redis模板
public RedisDistributedLock(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public String acquireLock(String lockKey, long expireTime, TimeUnit timeUnit) {
// 生成唯一标识(避免释放别人的锁)
String lockValue = UUID.randomUUID().toString();
// 用setIfAbsent实现原子性加锁(等价于Redis的SET NX EX命令)
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, timeUnit);
return success != null && success ? lockValue : null;
}
@Override
public boolean releaseLock(String lockKey, String lockValue) {
// 执行Lua脚本释放锁
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(RELEASE_LOCK_LUA_SCRIPT);
redisScript.setResultType(Long.class);
// 执行脚本:KEYS[1]是lockKey,ARGV[1]是lockValue
Long result = stringRedisTemplate.execute(
redisScript,
Collections.singletonList(lockKey),
lockValue
);
return result != null && result > 0;
}
@Override
public String tryAcquireLock(String lockKey, long waitTime, long expireTime, TimeUnit timeUnit) throws InterruptedException {
long startTime = System.currentTimeMillis();
long waitMillis = timeUnit.toMillis(waitTime);
String lockValue = UUID.randomUUID().toString();
while (true) {
// 尝试获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, expireTime, timeUnit);
if (success != null && success) {
return lockValue; // 获取成功
}
// 检查是否超时
long elapsedTime = System.currentTimeMillis() - startTime;
if (elapsedTime >= waitMillis) {
return null; // 超时未获取
}
// 短暂休眠后重试(避免频繁请求Redis)
Thread.sleep(100);
}
}
}
关键逻辑说明
- 获取锁:用setIfAbsent方法,同时设置过期时间,保证 “加锁 + 过期” 原子性。
- 释放锁:通过 Lua 脚本先判断锁的归属(避免误删别人的锁),再删除,保证操作原子性。
- 重试机制:tryAcquireLock方法中,超过等待时间则放弃,避免无限阻塞。
注解 + AOP:简化业务使用
为了让业务代码更简洁,咱们用注解 + AOP 实现自动加锁 / 释放锁。
定义锁注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLockAnnotation {
/**
* 锁的key(支持SpEL表达式,比如"order:{#orderId}")
*/
String lockKey();
/**
* 锁过期时间(默认5秒)
*/
long expireTime() default 5;
/**
* 时间单位(默认秒)
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 最大等待时间(默认0,即不等待)
*/
long waitTime() default 0;
}
实现 AOP 切面
@Aspect
@Component
@Slf4j
public class DistributedLockAspect {
private final DistributedLock distributedLock;
public DistributedLockAspect(DistributedLock distributedLock) {
this.distributedLock = distributedLock;
}
// 拦截带有@DistributedLockAnnotation的方法
@Around("@annotation(distributedLockAnnotation)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLockAnnotation distributedLockAnnotation) throws Throwable {
// 解析锁的key(支持SpEL表达式)
String lockKey = parseLockKey(joinPoint, distributedLockAnnotation.lockKey());
long expireTime = distributedLockAnnotation.expireTime();
TimeUnit timeUnit = distributedLockAnnotation.timeUnit();
long waitTime = distributedLockAnnotation.waitTime();
// 获取锁
String lockValue = waitTime <= 0
? distributedLock.acquireLock(lockKey, expireTime, timeUnit)
: distributedLock.tryAcquireLock(lockKey, waitTime, expireTime, timeUnit);
if (lockValue == null) {
log.warn("获取分布式锁失败,lockKey: {}", lockKey);
throw new RuntimeException("获取分布式锁失败");
}
try {
// 执行目标方法(业务逻辑)
return joinPoint.proceed();
} finally {
// 释放锁(必须在finally中执行)
boolean releaseSuccess = distributedLock.releaseLock(lockKey, lockValue);
if (!releaseSuccess) {
log.warn("释放分布式锁失败,lockKey: {}, lockValue: {}", lockKey, lockValue);
}
}
}
// 解析SpEL表达式,生成最终的lockKey
private String parseLockKey(ProceedingJoinPoint joinPoint, String lockKey) {
if (!lockKey.contains("#")) {
return lockKey; // 不包含SpEL表达式,直接返回
}
// 解析方法参数和SpEL表达式
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Object[] args = joinPoint.getArgs();
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
// 填充方法参数到SpEL上下文
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
context.setVariable(parameterNames[i], args[i]);
}
}
// 执行SpEL表达式,获取最终的lockKey
Expression expression = parser.parseExpression(lockKey);
return expression.getValue(context, String.class);
}
}
业务使用示例
在需要加锁的方法上添加注解即可,比如订单创建场景:
@Service
public class OrderService {
@DistributedLockAnnotation(
lockKey = "order:{#orderId}", // 锁的key,通过SpEL表达式动态生成
expireTime = 10, // 锁10秒后过期
waitTime = 3 // 最多等待3秒
)
public void createOrder(String orderId) {
// 业务逻辑:比如扣减库存、创建订单记录等
log.info("创建订单,orderId: {}", orderId);
// 模拟业务处理时间
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
测试验证:确保锁的可靠性
光写代码还不够,必须通过测试验证锁的功能是否正常。
编写测试类
@SpringBootTest
public class DistributedLockTest {
@Autowired
private DistributedLock distributedLock;
@Autowired
private OrderService orderService;
private final String lockKey = "test:lock";
private int count = 0; // 用于验证互斥性的计数器
/**
* 测试锁的互斥性:多线程抢锁,最终count应等于线程数
*/
@Test
public void testDistributedLock() throws InterruptedException {
int threadCount = 100; // 100个线程抢锁
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int index = i;
executorService.submit(() -> {
try {
// 尝试获取锁,最多等5秒,锁过期3秒
String lockValue = distributedLock.tryAcquireLock(lockKey, 5, 3, TimeUnit.SECONDS);
if (lockValue != null) {
count++; // 临界区操作
System.out.println("线程" + index + "获取锁成功,count: " + count);
Thread.sleep(100); // 模拟业务处理
distributedLock.releaseLock(lockKey, lockValue);
} else {
System.out.println("线程" + index + "获取锁失败");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println("最终count: " + count); // 若锁有效,count应等于100
}
/**
* 测试注解式锁的使用
*/
@Test
public void testAnnotationLock() throws InterruptedException {
int threadCount = 10;
CountDownLatch countDownLatch = new CountDownLatch(threadCount);
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < threadCount; i++) {
final int index = i;
executorService.submit(() -> {
try {
orderService.createOrder("ORDER_" + index);
} catch (Exception e) {
System.out.println("线程" + index + "执行失败: " + e.getMessage());
} finally {
countDownLatch.countDown();
}
});
}
countDownLatch.await();
executorService.shutdown();
}
}
测试注意事项
- 执行测试时,建议用redis-cli monitor命令监控 Redis 操作,确认锁的获取和释放是否符合预期。
- 重点观察testDistributedLock的最终 count 值,若等于线程数,说明锁的互斥性有效。
- 测试注解功能时,查看日志是否有序输出,避免出现并发问题。
进阶优化:让锁更完善
实际使用中,还可以根据需求添加以下功能:
看门狗机制(锁续期)
如果业务执行时间可能超过锁的过期时间,可以添加定时续期
// 在获取锁后启动定时任务,每隔一段时间(如过期时间的1/3)延长锁的过期时间
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 续期任务示例
private void startRenewTask(String lockKey, String lockValue, long expireTime, TimeUnit timeUnit) {
long period = timeUnit.toMillis(expireTime) / 3; // 每1/3过期时间续期一次
scheduler.scheduleAtFixedRate(() -> {
// 延长锁的过期时间
stringRedisTemplate.expire(lockKey, expireTime, timeUnit);
}, 0, period, TimeUnit.MILLISECONDS);
}
可重入锁支持
通过记录线程持有锁的次数,实现可重入性(同一线程可多次获取同一把锁):
// 用ThreadLocal记录当前线程持有的锁和次数
private ThreadLocal<Map<String, Integer>> lockCount = ThreadLocal.withInitial(HashMap::new);
Redis 集群适配
生产环境建议用 Redis 集群,避免单点故障,配置时只需修改 Redis 连接信息即可兼容。
总结
优势:轻量可控、无第三方依赖、按需定制功能。
注意事项:
- 必须保证锁的过期时间大于业务最大执行时间(或启用续期)。
- 释放锁时必须校验锁的归属(通过 UUID),避免误删。
- 高并发场景下,建议合理设置重试间隔,避免 Redis 压力过大。
这套方案已经能满足大部分中小型项目的需求,你可以根据自己的业务场景调整细节。如果还有其他疑问,或者想补充某个功能,欢迎在评论区交流!
相关推荐
- 订单超时自动取消业务的 N 种实现方案,从原理到落地全解析
-
在分布式系统架构中,订单超时自动取消机制是保障业务一致性的关键组件。某电商平台曾因超时处理机制缺陷导致日均3000+订单库存锁定异常,直接损失超50万元/天。本文将从技术原理、实现细节、...
- 使用Spring Boot 3开发时,如何选择合适的分布式技术?
-
作为互联网大厂的后端开发人员,当你满怀期待地用上SpringBoot3,准备在项目中大显身手时,却发现一个棘手的问题摆在面前:面对众多分布式技术,究竟该如何选择,才能让SpringBoot...
- 数据库内存爆满怎么办?99%的程序员都踩过这个坑!
-
你的数据库是不是又双叒叕内存爆满了?!服务器监控一片红色警告,老板在群里@所有人,运维同事的电话打爆了手机...这种场景是不是特别熟悉?别慌!作为一个在数据库优化这条路上摸爬滚打了10年的老司机,今天...
- springboot利用Redisson 实现缓存与数据库双写不一致问题
-
使用了Redisson来操作Redis分布式锁,主要功能是从缓存和数据库中获取商品信息,以下是针对并发时更新缓存和数据库带来不一致问题的解决方案1.基于读写锁和删除缓存策略在并发更新场景下,...
- 外贸独立站数据库炸了?对象缓存让你起死回生
-
上周黑五,一个客户眼睁睁看着服务器CPU飙到100%——每次页面加载要查87次数据库。这让我想起2024年Pantheon的测试:Redis缓存能把WooCommerce查询速度提升20倍。跨境电商最...
- 手把手教你在 Spring Boot3 里纯编码实现自定义分布式锁
-
为什么要自己实现分布式锁?你是不是早就受够了引入各种第三方依赖时的繁琐?尤其是分布式锁这块,每次集成Redisson或者Zookeeper,都得额外维护一堆配置,有时候还会因为版本兼容问题头疼半...
- 如何设计一个支持百万级实时数据推送的WebSocket集群架构?
-
面试解答:要设计一个支持百万级实时数据推送的WebSocket集群架构,需从**连接管理、负载均衡、水平扩展、容灾恢复**四个维度切入:连接层设计-**长连接优化**:采用Netty或Und...
- Redis数据结构总结——面试最常问到的知识点
-
Redis作为主流的nosql存储,面试时经常会问到。其主要场景是用作缓存,分布式锁,分布式session,消息队列,发布订阅等等。其存储结构主要有String,List,Set,Hash,Sort...
- skynet服务的缺陷 lua死循环
-
服务端高级架构—云风的skynet这边有一个关于云风skynet的视频推荐给大家观看点击就可以观看了!skynet是一套多人在线游戏的轻量级服务端框架,使用C+Lua开发。skynet的显著优点是,...
- 七年Java开发的一路辛酸史:分享面试京东、阿里、美团后的心得
-
前言我觉得有一个能够找一份大厂的offer的想法,这是很正常的,这并不是我们的饭后谈资而是每个技术人的追求。像阿里、腾讯、美团、字节跳动、京东等等的技术氛围与技术规范度还是要明显优于一些创业型公司...
- mysql mogodb es redis数据库之间的区别
-
1.MySQL应用场景概念:关系型数据库,基于关系模型,使用表和行存储数据。优点:支持ACID事务,数据具有很高的一致性和完整性。缺点:垂直扩展能力有限,需要分库分表等方式扩展。对于复杂的查询和大量的...
- redis,memcached,nginx网络组件
-
1.理解阻塞io,非阻塞io,同步io,异步io的区别2.理解BIO和AIO的区别io多路复用只负责io检测,不负责io操作阻塞io中的write,能写多少是多少,只要写成功就返回,譬如准备写500字...
- SpringBoot+Vue+Redis实现验证码功能
-
一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis...
- AWS MemoryDB 可观测最佳实践
-
AWSMemoryDB介绍AmazonMemoryDB是一种完全托管的、内存中数据存储服务,专为需要极低延迟和高吞吐量的应用程序而设计。它与Redis和Memcached相似,但具有更...
- 从0构建大型AI推荐系统:实时化引擎从工具到生态的演进
-
在AI浪潮席卷各行各业的今天,推荐系统正从幕后走向前台,成为用户体验的核心驱动力。本文将带你深入探索一个大型AI推荐系统从零起步的全过程,揭示实时化引擎如何从单一工具演进为复杂生态的关键路径。无论你是...
你 发表评论:
欢迎- 一周热门
-
-
Redis客户端 Jedis 与 Lettuce
-
高并发架构系列:Redis并发竞争key的解决方案详解
-
redis如何防止并发(redis如何防止高并发)
-
Java SE Development Kit 8u441下载地址【windows版本】
-
开源推荐:如何实现的一个高性能 Redis 服务器
-
redis安装与调优部署文档(WinServer)
-
Redis 入门 - 安装最全讲解(Windows、Linux、Docker)
-
一文带你了解 Redis 的发布与订阅的底层原理
-
Redis如何应对并发访问(redis控制并发量)
-
Oracle如何创建用户,表空间(oracle19c创建表空间用户)
-
- 最近发表
- 标签列表
-
- oracle位图索引 (74)
- oracle批量插入数据 (65)
- oracle事务隔离级别 (59)
- oracle主从同步 (56)
- oracle 乐观锁 (53)
- redis 命令 (83)
- php redis (97)
- redis 存储 (67)
- redis 锁 (74)
- 启动 redis (73)
- redis 时间 (60)
- redis 删除 (69)
- redis内存 (64)
- redis并发 (53)
- redis 主从 (71)
- redis同步 (53)
- redis结构 (53)
- redis 订阅 (54)
- redis 登录 (62)
- redis 面试 (58)
- redis问题 (54)
- 阿里 redis (67)
- redis的缓存 (57)
- lua redis (59)
- redis 连接池 (64)