Springboot – Prevent repeated submission (lock mechanism — local lock, distributed lock)

   To prevent duplicate submissions, it is mainly handled in the form of locks. If it is a stand-alone deployment, you can use a local cache lock (Guava). If it is a distributed deployment, you need to use a distributed lock (you can use zk distributed lock or redis distributed lock), the distributed lock in this article takes redis distributed lock as an example.

  1. Local lock (Guava)

  1, import dependency

 

org.springframework.boot
spring-boot-starter-aop


com.google.guava
guava
21.0

  2, custom local lock annotation

package com.example.demo.utils;


import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
String key()
default "";
//Expiration time, use local cache can be ignored, if you use redis It’s needed for caching.
int expire() default 5;
}

  3. Local lock annotation implementation

package com.example.demo.utils;


import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Configuration
public class LockMethodInterceptor {
//Define the cache, set the maximum number of caches and expiration date span>
private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build();

@Around(
"execution(public * *(..)) && @annotation(com.example.demo.utils.LocalLock)")
public Object 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("Do not repeat the request! ");
}
CACHE.put(key,key);
}
try{
return joinPoint.proceed();
}
catch (Throwable throwable){
throw new RuntimeException("Server Exception");
}
finally {

}
}

private String getKey(String keyExpress, Object[] args){
for (int i = 0; i ) {
keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString());
}
return keyExpress;
}

}

  4. Control layer

 @ResponseBody

@PostMapping(value
="/localLock")
@ApiOperation(value
="Submit verification test repeatedly-use local cache lock")
@ApiImplicitParams( {@ApiImplicitParam(paramType
="query", name = "token", value = "token", dataType = "String" )})
@LocalLock(key
= "localLock:test:arg[0]")
public String localLock(String token){

return "sucess====="+token;
}

  5. Test

   First request:

  Share picture

   has not expired, visit again:

  Share pictures

Second, Redis Distributed lock

  1, import dependency

   import aop dependency and redis dependency

  2, configuration

   configure redis connection information Yes

  3, custom distributed lock annotations

package< span style="color: #000000;"> com.example.demo.utils;


import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLock {
//redis lock prefix
String prefix() default "";
//redis lock expiration time
int expire() default 5;
//redis lock expiration time unit
TimeUnit timeUnit() default TimeUnit.SECONDS;
//redis key separator
String delimiter() default ":";
}

  4. Custom key rule annotation

   Because the redis key may have a multi-level structure, such as redistest:demo1:token This form of :kkk requires custom key rules.

package com.example.demo. utils;


import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
String name()
default "";
}

  5. Define the key generation strategy interface

package com.example.demo.service;


import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Service;

public interface CacheKeyGenerator {
//Get AOP parameters and generate designated cache Key
String getLockKey(ProceedingJoinPoint joinPoint);
}

  6. Define the key generation strategy implementation class

package com.example.demo.service.impl;


import com.example.demo.service.CacheKeyGenerator;
import com.example.demo.utils.CacheLock;
import com.example.demo.utils.CacheParam;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class CacheKeyGeneratorImp implements CacheKeyGenerator {
@Override
public String getLockKey(ProceedingJoinPoint joinPoint) {
//Get the method signature object of the connection point
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
//Method object
Method method = methodSignature.getMethod();
//Get the annotation object on the Method object
CacheLock cacheLock = method.getAnnotation(CacheLock.class);
//Get method parameters
final Object[] args = joinPoint.getArgs();
//Get all the annotations on the Method object
final Parameter[] parameters = method.getParameters();
StringBuilder sb
= new StringBuilder();
for(int i=0;i){
final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class );
//If the attribute is not a CacheParam annotation, it will not be processed
if(cacheParams == null){
continue;
}
//If the attribute is CacheParam annotation, splice the connector (: )+ CacheParam
sb.append(cacheLock.delimiter()).append(args[i]);
}
//If there is no CacheParam annotation on the method
if(StringUtils.isEmpty(sb.toString())){
//Get multiple annotations on the method (why two layers Array: because the second-level array is an array with only one element)
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
//Loop annotations
for(int i=0;i){
final Object object = args[i];
//Get all the attribute fields in the annotation class
final Field[] fields = object.getClass().getDeclaredFields();
for(Field field: fields){
// Determine whether there is CacheParam annotation on the field
final CacheParam annotation = field.getAnnotation(CacheParam.class);
//If not, skip it
if(annotation ==null){
continue;
}
//If yes, set Accessible to true (you can Use reflection to access private variables, otherwise you cannot access private variables)
field.setAccessible(true);
//If the attribute is CacheParam annotation, splice the connector (: )+ CacheParam
sb.append(cacheLock.delimiter()).append(ReflectionUtils.getField(field,object));
}
}
}
//returns the key of the specified prefix
return cacheLock.prefix() + sb.toString();
}
}

  7. Distributed annotation implementation

package com.example.demo.utils;


import com.example.demo.service.CacheKeyGenerator;
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.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration ;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

@Aspect
@Configuration
public class CacheLockMethodInterceptor {



@Autowired
public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){
this.cacheKeyGenerator = cacheKeyGenerator;
this.stringRedisTemplate = stringRedisTemplate;
}

private final StringRedisTemplate stringRedisTemplate;
private final CacheKeyGenerator cacheKeyGenerator;

@Around(
"execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)")
public Object 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("Prefix cannot be empty" );
}
//Get custom 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 logically we should throw a custom CacheLockException anomaly; steal the lazy here
throw new RuntimeException("Do not repeat the request");
}
try {
return joinPoint.proceed();
}
catch (Throwable throwable) {
throw new RuntimeException("System Exception");
}
}
}

  8. Main function adjustment

   Main function introduces key generation strategy

 @Bean

public CacheKeyGenerator cacheKeyGenerator(){
return new CacheKeyGeneratorImp();
}

  9, Controller

 @ResponseBody

@PostMapping(value
="/cacheLock")
@ApiOperation(value
="Re-submit verification test-use redis lock")
@ApiImplicitParams( {@ApiImplicitParam(paramType
="query", name = "token", value = "token", dataType = "String" )})
//@CacheLock
@CacheLock()
public String cacheLock(String token){
return "sucess====="+token;
}

@ResponseBody
@PostMapping(value
="/cacheLock1")
@ApiOperation(value
="Re-submit verification test-use redis lock")
@ApiImplicitParams( {@ApiImplicitParam(paramType
="query", name = "token", value = "token", dataType = "String" )})
//@CacheLock
@CacheLock(prefix = "redisLock.test",expire = 20)
public String cacheLock1(String token){
return "sucess====="+token;
}

@ResponseBody
@PostMapping(value
="/cacheLock2")
@ApiOperation(value
="Re-submit verification test-use redis lock")
@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. Test

   (1) Because the CacheLock annotation of the cacheLock method is not prefixed with prefix, an error will be reported

share picture

  (2) No CacheParam annotation added

   First call:

Share pictures

  Cache information:

  You can find that the key is the value of the priority

share picture

   Second call:

 Sharing pictures

   (3) Added CacheParam annotation

< p>   first call:

  share picture

  Cache information:

  You can find that the cached content is [emailprotected]

  share picture

   Second call:

share picture

 

org.springframework.boot
spring-boot-starter-aop


com.google.guava
guava
21.0

package com.example.demo.utils;


import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LocalLock {
String key()
default "";
//Expiration time, use local cache can be ignored, if you use redis It’s needed for caching.
int expire() default 5;
}

package com.example.demo.utils;


import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Configuration
public class LockMethodInterceptor {
//定义缓存,设置最大缓存数及过期日期
private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(20, TimeUnit.SECONDS).build();

@Around(
"execution(public * *(..)) && @annotation(com.example.demo.utils.LocalLock)")
public Object 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{
return joinPoint.proceed();
}
catch (Throwable throwable){
throw new RuntimeException("服务器异常");
}
finally {

}
}

private String getKey(String keyExpress, Object[] args){
for (int i = 0; i < args.length; i++) {
keyExpress
= keyExpress.replace("arg[" + i + "]", args[i].toString());
}
return keyExpress;
}

}

    @ResponseBody

@PostMapping(value
="/localLock")
@ApiOperation(value
="重复提交验证测试--使用本地缓存锁")
@ApiImplicitParams( {@ApiImplicitParam(paramType
="query", name = "token", value = "token", dataType = "String")})
@LocalLock(key
= "localLock:test:arg[0]")
public String localLock(String token){

return "sucess====="+token;
}

package com.example.demo.utils;


import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLock {
//redis锁前缀
String prefix() default "";
//redis锁过期时间
int expire() default 5;
//redis锁过期时间单位
TimeUnit timeUnit() default TimeUnit.SECONDS;
//redis key分隔符
String delimiter() default ":";
}

package com.example.demo.utils;


import java.lang.annotation.*;

@Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
String name()
default "";
}

package com.example.demo.service;


import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.stereotype.Service;

public interface CacheKeyGenerator {
//获取AOP参数,生成指定缓存Key
String getLockKey(ProceedingJoinPoint joinPoint);
}

package com.example.demo.service.impl;


import com.example.demo.service.CacheKeyGenerator;
import com.example.demo.utils.CacheLock;
import com.example.demo.utils.CacheParam;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

public class CacheKeyGeneratorImp implements CacheKeyGenerator {
@Override
public String 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
= new StringBuilder();
for(int i=0;i){
final CacheParam cacheParams = parameters[i].getAnnotation(CacheParam.class);
//如果属性不是CacheParam注解,则不处理
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 Object object = args[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();
}
}

package com.example.demo.utils;


import com.example.demo.service.CacheKeyGenerator;
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.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

@Aspect
@Configuration
public class CacheLockMethodInterceptor {



@Autowired
public CacheLockMethodInterceptor(StringRedisTemplate stringRedisTemplate, CacheKeyGenerator cacheKeyGenerator){
this.cacheKeyGenerator = cacheKeyGenerator;
this.stringRedisTemplate = stringRedisTemplate;
}

private final StringRedisTemplate stringRedisTemplate;
private final CacheKeyGenerator cacheKeyGenerator;

@Around(
"execution(public * * (..)) && @annotation(com.example.demo.utils.CacheLock)")
public Object 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 {
return joinPoint.proceed();
}
catch (Throwable throwable) {
throw new RuntimeException("系统异常");
}
}
}

    @Bean

public CacheKeyGenerator cacheKeyGenerator(){
return new CacheKeyGeneratorImp();
}

    @ResponseBody

@PostMapping(value
="/cacheLock")
@ApiOperation(value
="重复提交验证测试--使用redis锁")
@ApiImplicitParams( {@ApiImplicitParam(paramType
="query", name = "token", value = "token", dataType = "String")})
//@CacheLock
@CacheLock()
public String 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)
public String 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;
}

Leave a Comment

Your email address will not be published.