在 spring 生态中,并没有原生的 @lock 注解,但业界常用的做法是自定义 @lock 注解 + aop 切面 来封装锁逻辑(支持本地锁 / 分布式锁),让代码更简洁、可复用。下面我会完整讲解如何定义和使用 @lock 注解解决线程并发问题,包括本地锁版(单实例)和分布式锁版(多实例)两种核心场景。
一、核心思路
@lock 注解的本质是通过 aop 拦截标注了该注解的方法,在方法执行前加锁、执行后释放锁,从而保证方法执行的原子性,解决多线程竞争共享资源的问题。
二、场景 1:单实例下的 @lock(本地锁)
适用于单服务器部署的应用,使用 reentrantlock(可重入锁)实现。
步骤 1:定义 @lock 注解
import java.lang.annotation.*;
import java.util.concurrent.timeunit;
/**
* 自定义锁注解(本地锁)
* 用于标记需要加锁的方法,解决单实例并发问题
*/
@target(elementtype.method) // 仅作用于方法
@retention(retentionpolicy.runtime) // 运行时生效
@documented
public @interface lock {
/**
* 锁的唯一标识(支持spel表达式,如 "#userid")
* 为空时默认使用「类名.方法名」作为锁标识
*/
string key() default "";
/**
* 加锁超时时间(默认5秒)
* 超时未获取锁则抛出异常,避免线程阻塞
*/
long waittime() default 5;
/**
* 锁的自动释放时间(默认30秒)
* 防止死锁(reentrantlock本身不会死锁,但超时释放更安全)
*/
long leasetime() default 30;
/**
* 时间单位(默认秒)
*/
timeunit timeunit() default timeunit.seconds;
}步骤 2:实现 @lock 注解的 aop 切面(核心)
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.reflect.methodsignature;
import org.springframework.core.defaultparameternamediscoverer;
import org.springframework.expression.expressionparser;
import org.springframework.expression.spel.standard.spelexpressionparser;
import org.springframework.expression.spel.support.standardevaluationcontext;
import org.springframework.stereotype.component;
import java.lang.reflect.method;
import java.util.map;
import java.util.concurrent.concurrenthashmap;
import java.util.concurrent.locks.lock;
import java.util.concurrent.locks.reentrantlock;
/**
* @lock注解的切面实现(本地锁)
*/
@aspect // 标记为aop切面
@component // 交给spring容器管理
public class locallockaspect {
// 缓存锁实例:key=锁标识,value=reentrantlock对象(concurrenthashmap保证线程安全)
private final map<string, lock> lockcache = new concurrenthashmap<>();
// spel表达式解析器(解析注解中的key参数)
private final expressionparser spelparser = new spelexpressionparser();
// 参数名解析器(获取方法参数名,用于spel解析)
private final defaultparameternamediscoverer parameternamediscoverer = new defaultparameternamediscoverer();
/**
* 环绕通知:拦截所有标注了@lock的方法
*/
@around("@annotation(lock)")
public object around(proceedingjoinpoint joinpoint, lock lock) throws throwable {
// 1. 解析锁的唯一标识(key)
string lockkey = resolvelockkey(joinpoint, lock);
// 2. 获取或创建锁(不存在则新建,存在则复用)
lock reentrantlock = lockcache.computeifabsent(lockkey, k -> new reentrantlock());
// 3. 加锁(支持超时等待,避免线程无限阻塞)
boolean locked = false;
try {
locked = ((java.util.concurrent.locks.lock) reentrantlock).trylock(
lock.waittime(),
lock.leasetime(),
lock.timeunit()
);
if (!locked) {
throw new runtimeexception("获取锁失败,请稍后重试");
}
// 4. 执行原方法(核心业务逻辑)
return joinpoint.proceed();
} finally {
// 5. 释放锁(仅当前线程持有锁时释放,避免误释放)
if (locked && reentrantlock.isheldbycurrentthread()) {
reentrantlock.unlock();
}
}
}
/**
* 解析@lock注解中的key(支持spel表达式)
*/
private string resolvelockkey(proceedingjoinpoint joinpoint, lock lock) {
// 如果注解key为空,默认使用「类名.方法名」作为锁标识
if (lock.key().isempty()) {
return joinpoint.gettarget().getclass().getname() + "." + joinpoint.getsignature().getname();
}
// 解析spel表达式(如 "#userid" 解析为实际的用户id)
methodsignature signature = (methodsignature) joinpoint.getsignature();
method method = signature.getmethod();
string[] paramnames = parameternamediscoverer.getparameternames(method);
object[] args = joinpoint.getargs();
standardevaluationcontext context = new standardevaluationcontext();
for (int i = 0; i < paramnames.length; i++) {
context.setvariable(paramnames[i], args[i]);
}
return spelparser.parseexpression(lock.key()).getvalue(context, string.class);
}
}步骤 3:使用 @lock 注解解决并发问题
以 “库存扣减” 这个典型并发场景为例:
import org.springframework.stereotype.service;
@service
public class stockservice {
// 共享资源:模拟库存(多线程竞争的核心)
private int stock = 100;
/**
* 扣减库存(并发场景)
* @param productid 商品id(按商品id加锁,不同商品不互斥,提高并发效率)
* @param num 扣减数量
*/
@lock(key = "'stock:' + #productid") // spel表达式:锁标识为 "stock:商品id"
public void deductstock(long productid, int num) {
if (stock >= num) {
// 模拟耗时操作(如数据库查询/更新)
try {
thread.sleep(10);
} catch (interruptedexception e) {
thread.currentthread().interrupt();
}
stock -= num;
system.out.printf("商品%s扣减库存%d,剩余库存:%d%n", productid, num, stock);
} else {
throw new runtimeexception("商品" + productid + "库存不足");
}
}
// 获取库存(测试用)
public int getstock() {
return stock;
}
}步骤 4:测试并发效果
import org.springframework.context.annotation.annotationconfigapplicationcontext;
public class locktest {
public static void main(string[] args) {
// 初始化spring容器(扫描注解所在包)
annotationconfigapplicationcontext context =
new annotationconfigapplicationcontext("com.example");
stockservice stockservice = context.getbean(stockservice.class);
// 模拟10个线程并发扣减同一商品(productid=1)的库存(每个扣10)
for (int i = 0; i < 10; i++) {
new thread(() -> stockservice.deductstock(1l, 10)).start();
}
}
}测试结果(无并发问题,库存最终为 0):
商品1扣减库存10,剩余库存:90
商品1扣减库存10,剩余库存:80
商品1扣减库存10,剩余库存:70
...
商品1扣减库存10,剩余库存:0
三、场景 2:分布式下的 @lock(分布式锁)
如果应用部署在多台服务器(多实例),本地锁会失效,需要将 @lock 注解适配为分布式锁(基于 redis/redisson)。
步骤 1:引入 redisson 依赖(maven)
<!-- redisson:redis分布式锁的最佳实践 -->
<dependency>
<groupid>org.redisson</groupid>
<artifactid>redisson-spring-boot-starter</artifactid>
<version>3.23.3</version>
</dependency>步骤 2:修改 @lock 切面为分布式锁实现
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.reflect.methodsignature;
import org.redisson.api.rlock;
import org.redisson.api.redissonclient;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.core.defaultparameternamediscoverer;
import org.springframework.expression.expressionparser;
import org.springframework.expression.spel.standard.spelexpressionparser;
import org.springframework.expression.spel.support.standardevaluationcontext;
import org.springframework.stereotype.component;
import java.lang.reflect.method;
/**
* @lock注解的切面实现(分布式锁)
*/
@aspect
@component
public class distributedlockaspect {
@autowired
private redissonclient redissonclient; // 注入redisson客户端(springboot自动配置)
private final expressionparser spelparser = new spelexpressionparser();
private final defaultparameternamediscoverer parameternamediscoverer = new defaultparameternamediscoverer();
@around("@annotation(lock)")
public object around(proceedingjoinpoint joinpoint, lock lock) throws throwable {
// 1. 解析锁标识
string lockkey = resolvelockkey(joinpoint, lock);
// 2. 获取分布式锁
rlock redissonlock = redissonclient.getlock(lockkey);
// 3. 加锁
boolean locked = false;
try {
locked = redissonlock.trylock(
lock.waittime(),
lock.leasetime(),
lock.timeunit()
);
if (!locked) {
throw new runtimeexception("获取分布式锁失败,请稍后重试");
}
// 4. 执行业务方法
return joinpoint.proceed();
} finally {
// 5. 释放锁(仅当前线程持有锁时释放)
if (locked && redissonlock.isheldbycurrentthread()) {
redissonlock.unlock();
}
}
}
// 复用之前的resolvelockkey方法(解析spel)
private string resolvelockkey(proceedingjoinpoint joinpoint, lock lock) {
if (lock.key().isempty()) {
return joinpoint.gettarget().getclass().getname() + "." + joinpoint.getsignature().getname();
}
methodsignature signature = (methodsignature) joinpoint.getsignature();
method method = signature.getmethod();
string[] paramnames = parameternamediscoverer.getparameternames(method);
object[] args = joinpoint.getargs();
standardevaluationcontext context = new standardevaluationcontext();
for (int i = 0; i < paramnames.length; i++) {
context.setvariable(paramnames[i], args[i]);
}
return spelparser.parseexpression(lock.key()).getvalue(context, string.class);
}
}步骤 3:分布式场景下使用 @lock
用法和本地锁完全一致,无需修改业务代码:
@service
public class orderservice {
/**
* 提交订单(分布式并发场景)
* @param orderid 订单id(按订单id加锁,不同订单不互斥)
*/
@lock(key = "'order:' + #orderid", leasetime = 10) // 锁过期时间10秒
public void submitorder(string orderid) {
// 分布式并发操作:扣减库存、生成订单记录
system.out.println("订单" + orderid + "提交成功");
}
}四、关键注意事项
- 锁粒度要合理:
- 避免使用全局锁(如固定 key),否则会导致所有请求串行,降低并发效率;
- 建议按业务维度加锁(如商品 id、订单 id),仅让同一业务维度的请求互斥。
- 防止死锁:
- 必须在
finally块中释放锁,确保无论方法是否异常,锁都会释放; - 分布式锁必须设置
leasetime(自动过期时间),防止服务宕机导致锁无法释放。
- 必须在
- spel 表达式规范:
- 字符串常量需要用单引号包裹(如
'stock:' + #productid),否则会解析失败。
- 字符串常量需要用单引号包裹(如
总结
@lock注解是通过 aop 切面 + 锁机制 实现的 “语法糖”,核心是在方法执行前后加锁 / 释放锁,保证原子性;- 单实例场景下,
@lock适配reentrantlock(本地锁),多实例场景下适配 redisson(分布式锁),业务代码无需修改; - 使用
@lock时需注意锁粒度(按业务参数加锁)和防死锁(finally 释放、设置过期时间),这是解决并发问题的关键。
到此这篇关于spring中使用自定义@lock 注解解决线程并发的核心思路的文章就介绍到这了,更多相关spring使用自定义@lock 注解内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论