目录
前言
思路
实现方式
实践
1.引入相关依赖
2.aop注解
3.切面类代码
4.由于启动时报错找不到对应的RedisLockRegistry bean,选择通过配置类手动注入,配置类代码如下
测试
章末
前言
项目中有个用户根据二维码绑定身份的接口,由于用户在操作时,可能会因为网络延迟或者其他原因多次点击提交按钮,导致重复提交相同的请求,所以需要在一定时间内限制同一个用户相同操作的重复提交,避免重复绑定的情况发生
思路
通过Spring 的aop 功能加上分布式锁实现,aop功能可以实现切面操作有关接口,再通过分布式锁实现同一个请求在一段时间内只执行一次,保证操作的幂等性,避免数据异常
实现方式
分布式锁选择的是 RedisLockRegistry,下面是该锁的简单介绍
RedisLockRegistry
是 Spring Integration 提供的一个基于 Redis 实现的分布式锁实用程序。可以用于在分布式环境中实现对共享资源的互斥访问。
RedisLockRegistry
使用 Redis 的原子性操作和过期时间设置来实现分布式锁。通过在 Redis 中创建一个特定的键(key),并在获取锁时将该键设置为具有过期时间的值(value)。其他线程或进程通过尝试在同一键上执行相同操作,如果能够设置成功,则表示获取到了锁,可以执行操作;否则,表示锁被其他线程或进程占用,需要等待。
实践
1.引入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
2.aop注解
这里有两个方法,一个是提供获取锁可重试时常,另一个是获得指定的key
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** 防止重复提交,通过分布式锁,限制同一个api接口并发时多次重复提交
* @author ben.huang
*/
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AntiReplay {
/**
* 获取锁重试时间,默认0ms,也就是不许重试,加锁失败,立即返回
* 产生竞争时,重试获取锁的最长等待时间,在改时间内如果没有获取到锁,则失败
* @return
*/
int tryLockTime() default 0;
/**
* 自定义的Key,不填的话默认“”,代码中可以自定义拼接
* 需要自己提供默认的话,在注解使用时赋值即可
* @return
*/
String key() default "";
}
3.切面类代码
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.integration.redis.util.RedisLockRegistry;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* @author ben.huang
*/
@Component
@Aspect
@Slf4j
public class AntiReplayAspect {
@Resource
private RedisLockRegistry redisLockRegistry;
@Pointcut("@annotation(antiReplay)")
public void pointcut(AntiReplay antiReplay){
}
@Around(value = "pointcut(antiReplay)")
public Object around(ProceedingJoinPoint proceedingJoinPoint,AntiReplay antiReplay) throws Throwable{
int tryLockTime = antiReplay.tryLockTime();
Object result = null;
String name = "testRedisLock-";
String path = antiReplay.key();
//这里简化了,使用时可以使用用户唯一辨识(比如用户id)拼接key
String key = name + path;
Lock lock = redisLockRegistry.obtain(key);
boolean isSuccess = lock.tryLock(tryLockTime, TimeUnit.MILLISECONDS);
if(isSuccess){
log.info("获取锁 key = [{}]",key);
try{
result = proceedingJoinPoint.proceed();
}
finally{
if(isSuccess){
lock.unlock();
log.info("释放锁 success, key = [{}]",key);
}
}
}
else{
log.info("获取锁失败 fial ,key = [{}]",key);
throw new Exception("error");
}
return result;
}
}
4.由于启动时报错找不到对应的RedisLockRegistry bean,选择通过配置类手动注入,配置类代码如下
Description: A component required a bean of type 'org.springframework.integration.redis.util.RedisLockRegistry' that could not be found.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;
/**
* @author ben.huang
*/
@Configuration
public class AntiReplayConfig {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory){
RedisLockRegistry redisLockRegistry = new RedisLockRegistry(redisConnectionFactory, "my-lock-key");
return redisLockRegistry;
}
}
测试
1.因为该场景是在并发时发生的,所以可以选择压测的方式模拟下并发场景,创建一个简单的测试接口,登录成功则在控制台打印信息,否则抛出异常
@AntiReplay(key = "userLogin")
@PostMapping(value = "/login")
public BaseResult login(String username, String password) {
UserEntity user = userService.selectOne(new EntityWrapper<UserEntity>().eq("username", username));
if(user==null || !user.getPassword().equals(password)) {
throw new RuntimeException("error while logining");
}
System.out.println(" login success!");
return BaseResult.success();
}
2.压测工具使用的是APIpost接口测试工具,不加防重复注解时启动项目调用接口的结果如下
可以看到在没有加锁的情况下,所有请求全部成功
3.加上注解后再次压测,结果如下 ,可以看到大部分请求都失败了,为什么还有这么多请求成功的?是因为获取到锁的线程会释放锁,后面的线程还可以接着抢,看控制台也会发现有很多次释放锁记录
章末
文章到这里就结束了~