下面将详细介绍如何在 Spring Boot 里借助注解实现分布式锁,以login_lock:
作为锁的 key 前缀,使用请求参数里的phone
值作为 key,等待时间设为 0 秒,锁的持续时间为 10 秒。我们会使用 Redis 来实现分布式锁,同时借助 Spring AOP 与自定义注解达成基于注解的锁机制。
1. 添加依赖
在pom.xml
文件中添加必要的依赖,包括 Spring Boot Redis 和 Spring Boot AOP:
xml
XML
<dependencies>
<!-- Spring Boot Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Spring Boot AOP 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
2. 配置 Redis
在application.properties
或者application.yml
中配置 Redis 连接信息,以application.yml
为例:
yaml
XML
spring:
redis:
host: localhost
port: 6379
# 若 Redis 有密码,需添加此项
# password: yourpassword
3. 定义分布式锁注解
创建自定义注解DistributedLock
,用于标记需要加锁的方法:
java
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String keyPrefix() default "login_lock:";
String keyField() default "phone";
long waitTime() default 0;
long leaseTime() default 10;
}
4. 实现分布式锁切面
创建切面类DistributedLockAspect
,处理加锁和解锁逻辑:
java
java
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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class DistributedLockAspect {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final RedisScript<Long> UNLOCK_SCRIPT;
static {
StringBuilder script = new StringBuilder();
script.append("if redis.call('get', KEYS[1]) == ARGV[1] then");
script.append(" return redis.call('del', KEYS[1])");
script.append("else");
script.append(" return 0");
script.append("end");
UNLOCK_SCRIPT = new DefaultRedisScript<>(script.toString(), Long.class);
}
@Around("@annotation(com.example.demo.DistributedLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
DistributedLock distributedLock = signature.getMethod().getAnnotation(DistributedLock.class);
String keyPrefix = distributedLock.keyPrefix();
String keyField = distributedLock.keyField();
long waitTime = distributedLock.waitTime();
long leaseTime = distributedLock.leaseTime();
Object[] args = joinPoint.getArgs();
String keyValue = null;
for (Object arg : args) {
try {
Field field = arg.getClass().getDeclaredField(keyField);
field.setAccessible(true);
keyValue = String.valueOf(field.get(arg));
break;
} catch (NoSuchFieldException | IllegalAccessException e) {
// 忽略异常,继续尝试下一个参数
}
}
if (keyValue == null) {
throw new IllegalArgumentException("Could not find the key field in the method arguments.");
}
String lockKey = keyPrefix + keyValue;
String requestId = java.util.UUID.randomUUID().toString();
long startTime = System.currentTimeMillis();
boolean locked = false;
do {
locked = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, leaseTime, TimeUnit.SECONDS);
if (locked) {
break;
}
// 检查是否超过等待时间
if (System.currentTimeMillis() - startTime >= waitTime * 1000) {
break;
}
// 短暂休眠后重试
Thread.sleep(100);
} while (true);
if (!locked) {
throw new RuntimeException("Failed to acquire the lock after waiting.");
}
try {
return joinPoint.proceed();
} finally {
// 使用 Lua 脚本释放锁,保证原子性
redisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(lockKey), requestId);
}
}
}
5. 使用分布式锁注解
在需要加锁的方法上添加DistributedLock
注解:
java
java
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class LoginController {
@PostMapping("/login")
@DistributedLock
public String login(@RequestBody User user) {
// 模拟业务逻辑
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Login success";
}
}
class User {
private String phone;
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
代码解释
DistributedLock
注解 :用于标记需要加锁的方法,可通过keyPrefix
、keyField
、waitTime
和leaseTime
属性配置锁的相关信息。DistributedLockAspect
切面类 :- 运用
@Around
注解拦截所有标记了DistributedLock
注解的方法。 - 从方法参数里提取
phone
值,组合成 Redis 锁的 key。 - 借助
redisTemplate.opsForValue().setIfAbsent
方法尝试获取锁,若获取失败则抛出异常。 - 使用 Lua 脚本释放锁,确保操作的原子性,防止误删其他线程的锁。
- 运用
LoginController
控制器 :在login
方法上添加DistributedLock
注解,保证同一手机号在同一时间只有一个请求能进入该方法。
通过以上步骤,你就能在 Spring Boot 项目中使用注解实现分布式锁。