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

相关推荐
记录测试点滴19 分钟前
【中间件】 Kafka
分布式·中间件·kafka
极客先躯20 分钟前
高级java每日一道面试题-2025年01月27日-框架篇[SpringBoot篇]-如何在Spring Boot启动的时候运行一些特定的代码?
java·spring boot·后端·初始化·启动执行
disgare22 分钟前
设计模式——状态模式
java·设计模式·状态模式
m0_7482359524 分钟前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘(上)
java·spring boot·后端
m0_674031431 小时前
Spring boot启动原理及相关组件
数据库·spring boot·后端
Gao__xi1 小时前
面试题-SpringCloud的启动流程
java·spring boot·spring cloud
苏-言1 小时前
Spring Boot Web项目全解析:从前端请求到后端处理
前端·spring boot·后端
青云交1 小时前
Java 大视界 -- Java 大数据在智能安防中的应用与创新(73)
java·大数据·机器学习·数据采集·数据存储·智能安防·视频监控分析
PlanOne_A1 小时前
【Redisson分布式锁】基于redisson的分布式锁
分布式
P7进阶路1 小时前
Tomcat Request Cookie 丢失问题
java·tomcat·firefox