Java 利用锁避免重复订票_SpringBoot--防止重复提交(锁机制---本地锁、分布式锁)...

防止重复提交,主要是使用锁的形式来处理,如果是单机部署,可以使用本地缓存锁(Guava)即可,如果是分布式部署,则需要使用分布式锁(可以使用zk分布式锁或者redis分布式锁),本文的分布式锁以redis分布式锁为例。

一、本地锁(Guava)

1、导入依赖

org.springframework.boot

spring-boot-starter-aop

com.google.guava

guava

21.0

2、自定义本地锁注解

packagecom.example.demo.utils;import java.lang.annotation.*;

@Target({ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inheritedpublic @interfaceLocalLock {

String key()default "";//过期时间,使用本地缓存可以忽略,如果使用redis做缓存就需要

int expire() default 5;

}

3、本地锁注解实现

packagecom.example.demo.utils;importcom.google.common.cache.Cache;importcom.google.common.cache.CacheBuilder;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.Signature;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.context.annotation.Configuration;importorg.springframework.util.StringUtils;importjava.lang.reflect.Method;importjava.util.concurrent.TimeUnit;

@Aspect

@Configurationpublic classLockMethodInterceptor {//定义缓存,设置最大缓存数及过期日期

private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build();

@Around("execution(public * *(..)) && @annotation(com.example.demo.utils.LocalLock)")publicObject interceptor(ProceedingJoinPoint joinPoint){

MethodSignature signature=(MethodSignature) joinPoint.getSignature();

Method method=signature.getMethod();

LocalLock localLock= method.getAnnotation(LocalLock.class);

String key=getKey(localLock.key(),joinPoint.getArgs());if(!StringUtils.isEmpty(key)){if(CACHE.getIfPresent(key) != null){throw new RuntimeException("请勿重复请求!");

}

CACHE.put(key,key);

}try{returnjoinPoint.proceed();

}catch(Throwable throwable){throw new RuntimeException("服务器异常");

}finally{

}

}privateString getKey(String keyExpress, Object[] args){for (int i = 0; i < args.length; i++) {

keyExpress= keyExpress.replace("arg[" + i + "]", args[i].toString());

}returnkeyExpress;

}

}

4、控制层

@ResponseBody

@PostMapping(value="/localLock")

@ApiOperation(value="重复提交验证测试--使用本地缓存锁")

@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})

@LocalLock(key= "localLock:test:arg[0]")publicString localLock(String token){return "sucess====="+token;

}

5、测试

第一次请求:

1a890ce5140c35f5dba04e74de5ecccc.png

未过期再次访问:

d107b7ec38d39908772db27954d56ace.png

二、Redis分布式锁

1、导入依赖

导入aop依赖和redis依赖即可

2、配置

配置redis连接信息即可

3、自定义分布式锁注解

packagecom.example.demo.utils;import java.lang.annotation.*;importjava.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inheritedpublic @interfaceCacheLock {//redis锁前缀

String prefix() default "";//redis锁过期时间

int expire() default 5;//redis锁过期时间单位

TimeUnit timeUnit() defaultTimeUnit.SECONDS;//redis key分隔符

String delimiter() default ":";

}

4、自定义key规则注解

由于redis的key可能是多层级结构,例如 redistest:demo1:token:kkk这种形式,因此需要自定义key的规则。

packagecom.example.demo.utils;import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Inheritedpublic @interfaceCacheParam {

String name()default "";

}

5、定义key生成策略接口

packagecom.example.demo.service;importorg.aspectj.lang.ProceedingJoinPoint;importorg.springframework.stereotype.Service;public interfaceCacheKeyGenerator {//获取AOP参数,生成指定缓存Key

String getLockKey(ProceedingJoinPoint joinPoint);

}

6、定义key生成策略实现类

packagecom.example.demo.service.impl;importcom.example.demo.service.CacheKeyGenerator;importcom.example.demo.utils.CacheLock;importcom.example.demo.utils.CacheParam;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.util.ReflectionUtils;importorg.springframework.util.StringUtils;importjava.lang.annotation.Annotation;importjava.lang.reflect.Field;importjava.lang.reflect.Method;importjava.lang.reflect.Parameter;public class CacheKeyGeneratorImp implementsCacheKeyGenerator {

@OverridepublicString getLockKey(ProceedingJoinPoint joinPoint) {//获取连接点的方法签名对象

MethodSignature methodSignature =(MethodSignature) joinPoint.getSignature();//Method对象

Method method =methodSignature.getMethod();//获取Method对象上的注解对象

CacheLock cacheLock = method.getAnnotation(CacheLock.class);//获取方法参数

final Object[] args =joinPoint.getArgs();//获取Method对象上所有的注解

final Parameter[] parameters =method.getParameters();

StringBuilder sb= newStringBuilder();for(int i=0;i

if(cacheParams == null){continue;

}//如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam

sb.append(cacheLock.delimiter()).append(args[i]);

}//如果方法上没有加CacheParam注解

if(StringUtils.isEmpty(sb.toString())){//获取方法上的多个注解(为什么是两层数组:因为第二层数组是只有一个元素的数组)

final Annotation[][] parameterAnnotations =method.getParameterAnnotations();//循环注解

for(int i=0;i

final Field[] fields =object.getClass().getDeclaredFields();for(Field field : fields){//判断字段上是否有CacheParam注解

final CacheParam annotation = field.getAnnotation(CacheParam.class);//如果没有,跳过

if(annotation ==null){continue;

}//如果有,设置Accessible为true(为true时可以使用反射访问私有变量,否则不能访问私有变量)

field.setAccessible(true);//如果属性是CacheParam注解,则拼接 连接符(:)+ CacheParam

sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object));

}

}

}//返回指定前缀的key

return cacheLock.prefix() +sb.toString();

}

}

7、分布式注解实现

packagecom.example.demo.utils;importcom.example.demo.service.CacheKeyGenerator;importorg.aspectj.lang.ProceedingJoinPoint;importorg.aspectj.lang.annotation.Around;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.reflect.MethodSignature;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisStringCommands;importorg.springframework.data.redis.core.RedisCallback;importorg.springframework.data.redis.core.StringRedisTemplate;importorg.springframework.data.redis.core.types.Expiration;importorg.springframework.util.StringUtils;importjava.lang.reflect.Method;

@Aspect

@Configurationpublic classCacheLockMethodInterceptor {

@AutowiredpublicCacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){this.cacheKeyGenerator =cacheKeyGenerator;this.stringRedisTemplate =stringRedisTemplate;

}private finalStringRedisTemplate stringRedisTemplate;private finalCacheKeyGenerator cacheKeyGenerator;

@Around("execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)")publicObject interceptor(ProceedingJoinPoint joinPoint){

MethodSignature methodSignature=(MethodSignature) joinPoint.getSignature();

Method method=methodSignature.getMethod();

CacheLock cacheLock= method.getAnnotation(CacheLock.class);if(StringUtils.isEmpty(cacheLock.prefix())){throw new RuntimeException("前缀不能为空");

}//获取自定义key

final String lockkey =cacheKeyGenerator.getLockKey(joinPoint);final Boolean success =stringRedisTemplate.execute(

(RedisCallback) connection -> connection.set(lockkey.getBytes(), new byte[0], Expiration.from(cacheLock.expire(), cacheLock.timeUnit())

, RedisStringCommands.SetOption.SET_IF_ABSENT));if (!success) {//TODO 按理来说 我们应该抛出一个自定义的 CacheLockException 异常;这里偷下懒

throw new RuntimeException("请勿重复请求");

}try{returnjoinPoint.proceed();

}catch(Throwable throwable) {throw new RuntimeException("系统异常");

}

}

}

8、主函数调整

主函数引入key生成策略

@BeanpublicCacheKeyGenerator cacheKeyGenerator(){return newCacheKeyGeneratorImp();

}

9、Controller

@ResponseBody

@PostMapping(value="/cacheLock")

@ApiOperation(value="重复提交验证测试--使用redis锁")

@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})//@CacheLock

@CacheLock()publicString cacheLock(String token){return "sucess====="+token;

}

@ResponseBody

@PostMapping(value="/cacheLock1")

@ApiOperation(value="重复提交验证测试--使用redis锁")

@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})//@CacheLock

@CacheLock(prefix = "redisLock.test",expire = 20)publicString cacheLock1(String token){return "sucess====="+token;

}

@ResponseBody

@PostMapping(value="/cacheLock2")

@ApiOperation(value="重复提交验证测试--使用redis锁")

@ApiImplicitParams( {@ApiImplicitParam(paramType="query", name = "token", value = "token", dataType = "String")})//@CacheLock

@CacheLock(prefix = "redisLock.test",expire = 20)public String cacheLock2(@CacheParam(name = "token") String token){return "sucess====="+token;

}

10、测试

(1)由于cacheLock方法的CacheLock注解没有加prefix前缀,因此会报错

48d75cc6ce17dcd6bc3d945c5458a9d4.png

(2)没有加CacheParam注解

第一次调用:

e421b6812d4ba3cb9712e4075d2846e1.png

缓存信息:

可以发现key为prifix的值

1ae011e8162b4b90345ec49468381caf.png

第二次调用:

325b8b5f9daa31d3621f23692472a6ea.png

(3)增加了CacheParam注解

第一次调用:

996484f004a8a6fa3f1d66a92c73dbe8.png

缓存信息:

可以发现缓存的内容为prefix+@CacheParam

f32bc1c22987a28bbb63a9a660f8fe98.png

第二次调用:

4244dcd2a5725860ce6236f971e683f3.png


版权声明:本文为weixin_29545089原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。