springboot中使用注解实现分布式锁

下面将详细介绍如何在 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注解 :用于标记需要加锁的方法,可通过keyPrefixkeyFieldwaitTimeleaseTime属性配置锁的相关信息。
  • DistributedLockAspect切面类
    • 运用@Around注解拦截所有标记了DistributedLock注解的方法。
    • 从方法参数里提取phone值,组合成 Redis 锁的 key。
    • 借助redisTemplate.opsForValue().setIfAbsent方法尝试获取锁,若获取失败则抛出异常。
    • 使用 Lua 脚本释放锁,确保操作的原子性,防止误删其他线程的锁。
  • LoginController控制器 :在login方法上添加DistributedLock注解,保证同一手机号在同一时间只有一个请求能进入该方法。

通过以上步骤,你就能在 Spring Boot 项目中使用注解实现分布式锁。

相关推荐
C_Liu_9 分钟前
C语言:深入理解指针(5)
java·c语言·算法
佛祖保佑永不宕机42 分钟前
麒麟系统ARM64架构部署mysql、jdk和java项目
java·arm
qqxhb2 小时前
零基础学Java——第十一章:实战项目 - 桌面应用开发(JavaFX入门)
java·开发语言·javafx
@小了白了兔2 小时前
RabbitMQ工作流程及使用方法
分布式·rabbitmq
hy.z_7772 小时前
【数据结构】链表 LinkedList
java·数据结构·链表
松树戈2 小时前
plus-ui&RuoYi-Vue-Plus 基于pgSql本地运行实践
前端·vue.js·spring boot·ui
Akiiiira2 小时前
【数据结构】队列
java·开发语言·数据结构
程序媛学姐2 小时前
Java级联操作:CascadeType的选择与最佳实践
java·开发语言
dddaidai1232 小时前
分布式ID和分布式锁
redis·分布式·mysql·zookeeper·etcd
不知几秋3 小时前
Maven
java·数据库·maven