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 项目中使用注解实现分布式锁。

相关推荐
毕设源码-朱学姐2 分钟前
【开题答辩全过程】以 日程管理系统为例,包含答辩的问题和答案
java
a努力。5 分钟前
京东Java面试被问:双亲委派模型被破坏的场景和原理
java·开发语言·后端·python·面试·linq
小毛驴85011 分钟前
Maven同时配置阿里云仓库和私有仓库
java·阿里云·maven
刘975312 分钟前
【第25天】25c#今日小结
java·开发语言·c#
不如打代码KK15 分钟前
Springboot如何解决跨域问题?
java·spring boot·后端
豆沙沙包?17 分钟前
2026年--Lc330-394. 字符串解码(栈)--java版
java·开发语言
蓝程序17 分钟前
Spring AI学习 程序接入大模型
java·人工智能·spring
nice_lcj52018 分钟前
数据结构之树与二叉树:重点梳理与拓展
java·数据结构
毕设源码-钟学长19 分钟前
【开题答辩全过程】以 助学贷款管理系统为例,包含答辩的问题和答案
java
亓才孓21 分钟前
任意大小的整数和任意精度的小数的API方法
java