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

相关推荐
chenOnlyOne2 小时前
Spring Boot实战:MySQL与Redis数据一致性深度解析与代码实战
spring boot·redis·mysql
失业写写八股文4 小时前
Spring基础:Spring的事物哪些情况下会失效
java·后端·spring
吧啦吧啦吡叭卜7 小时前
【打卡d5】快速排序 归并排序
java·算法·排序算法
问道飞鱼7 小时前
【Springboot知识】开发属于自己的中间件健康监测HealthIndicate
spring boot·后端·中间件·healthindicate
大得3697 小时前
宝塔docker切换存储目录
java·docker·eureka
洛北辰南8 小时前
系统架构设计师—案例分析—数据库篇—分布式缓存技术
数据库·分布式·系统架构·缓存技术
东阳马生架构8 小时前
Netty基础—4.NIO的使用简介一
java·网络·netty
luckyext8 小时前
Postman用JSON格式数据发送POST请求及注意事项
java·前端·后端·测试工具·c#·json·postman
程序视点8 小时前
Redis集群机制及一个Redis架构演进实例
java·redis·后端
鱼樱前端8 小时前
Navicat17基础使用
java·后端