避免重复调用的最主要的一个思路是(大家拿起笔做笔记):设置一个独一无二的 <key, value> 格式的数据,设置一个过期时间(或者方法执行完毕后直接删除<key, value>),然后去锁定这个key,如果成功则放行,失败则提示重复调用。
以下实战代码来自于参考资料,但是关于 redis 的操作我使用了最原始的 jedis,jedis 的加锁解锁保证在分布式环境下是可行的,虽然我的思想很朴素,但是就是这么实现的。
在实际实现的过程中:使用拦截器来实现,这样可防止多余的编码,然后配合注解在 Controller 上的方法声明(这一步其实可有可无)。
CacheLock.java
package com.zhuoli.service.springboot.distributed.repeat.submit.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @Author: zhuoli
* @Date: 2018/9/14 13:44
* @Description:
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheLock {
/**
* redis 锁key的前缀
*/
String prefix() default "";
/**
* redis key过期时间
*/
int expire() default 5;
/**
* 超时时间单位
*
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* Key分隔符
* 比如:Key:1
*/
String delimiter() default ":";
}
CacheParam.java
package com.zhuoli.service.springboot.distributed.repeat.submit.annotation;
import java.lang.annotation.*;
/**
* @Author: zhuoli
* @Date: 2018/9/14 13:46
* @Description:
*/
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {
/**
* 字段名称
*
* @return String
*/
String name() default "";
}
LockMethodInterceptor.java
package com.zhuoli.service.springboot.distributed.repeat.submit.aop;
import com.zhuoli.service.springboot.distributed.repeat.submit.annotation.CacheLock;
import com.zhuoli.service.springboot.distributed.repeat.submit.common.keygenerator.CacheKeyGenerator;
import com.zhuoli.service.springboot.distributed.repeat.submit.common.redis.RedisLockHelper;
import lombok.AllArgsConstructor;
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.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* @Author: zhuoli
* @Date: 2018/9/14 13:59
* @Description:
*/
@Aspect
@Configuration
@AllArgsConstructor
public class LockMethodInterceptor {
private final RedisLockHelper redisLockHelper;
private final CacheKeyGenerator cacheKeyGenerator;
@Around("execution(public * *(..)) && @annotation(com.zhuoli.service.springboot.distributed.repeat.submit.annotation.CacheLock)")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
CacheLock lock = method.getAnnotation(CacheLock.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key don't null...");
}
final String lockKey = cacheKeyGenerator.getLockKey(pjp);
String value = UUID.randomUUID().toString();
try {
final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
if (!success) {
throw new RuntimeException("重复提交");
}
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("系统异常");
}
} finally {
//如果演示的话需要注释该代码,实际应该放开
//redisLockHelper.unlock(lockKey, value);
}
}
}
CacheKeyGenerator.java
package com.zhuoli.service.springboot.distributed.repeat.submit.common.keygenerator;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @Author: zhuoli
* @Date: 2018/9/14 13:48
* @Description:
*/
public interface CacheKeyGenerator {
/**
* 获取AOP参数,生成指定缓存Key
*
* @param pjp PJP
* @return 缓存KEY
*/
String getLockKey(ProceedingJoinPoint pjp);
}
LockKeyGenerator.java
package com.zhuoli.service.springboot.distributed.repeat.submit.common.keygenerator.impl;
import com.zhuoli.service.springboot.distributed.repeat.submit.annotation.CacheLock;
import com.zhuoli.service.springboot.distributed.repeat.submit.annotation.CacheParam;
import com.zhuoli.service.springboot.distributed.repeat.submit.common.keygenerator.CacheKeyGenerator;
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;
/**
* @Author: zhuoli
* @Date: 2018/9/14 13:49
* @Description:
*/
public class LockKeyGenerator implements CacheKeyGenerator {
@Override
public String getLockKey(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
CacheLock lockAnnotation = method.getAnnotation(CacheLock.class);
final Object[] args = pjp.getArgs();
final Parameter[] parameters = method.getParameters();
StringBuilder builder = new StringBuilder();
//默认解析方法里面带CacheParam注解的属性,如果没有尝试着解析实体对象中的CacheParam注解属性
for (int i = 0; i < parameters.length; i++) {
final CacheParam annotation = parameters[i].getAnnotation(CacheParam.class);
if (annotation == null) {
continue;
}
builder.append(lockAnnotation.delimiter()).append(args[i]);
}
if (StringUtils.isEmpty(builder.toString())) {
//CacheLock注解的方法参数没有CacheParam注解,则迭代解析参数实体中的CacheParam注解属性
final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = 0; i < parameterAnnotations.length; i++) {
final Object object = args[i];
final Field[] fields = object.getClass().getDeclaredFields();
for (Field field : fields) {
final CacheParam annotation = field.getAnnotation(CacheParam.class);
if (annotation == null) {
continue;
}
field.setAccessible(true);
builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field, object));
}
}
}
return lockAnnotation.prefix() + builder.toString();
}
}
RedisConfig.java
package com.zhuoli.service.springboot.distributed.repeat.submit.common.redis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
@Slf4j
public class RedisConfig {
/** 这些配置之类的可以放到配置文件 **/
private String host = "127.0.0.1";
private int port = 6379;
@Bean
public JedisPool redisPoolFactory() throws Exception{
log.info("JedisPool注入成功!!");
log.info("redis地址:" + host + ":" + port);
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 是否启用pool的jmx管理功能, 默认true
jedisPoolConfig.setJmxEnabled(true);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port);
return jedisPool;
}
}
RedisLockHelper.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.Jedis;
/**
* @Author: xushu
* @Date: 2018/9/14 14:01
* @Description:
*/
@Configuration
public class RedisLockHelper {
@Autowired
private RedisConfig redisConfig;
public boolean lock(String lockKey, final String uuid, long timeout) {
Jedis jedis = null;
try {
jedis = redisConfig.redisPoolFactory().getResource();
} catch (Exception e) {
e.printStackTrace();
}
String result = jedis.set(lockKey, uuid, "NX", "EX", timeout);
if("OK".equals(result)){
return true;
}
return false;
}
public void unlock(String lockKey, String value) {
Jedis jedis = null;
try {
jedis = redisConfig.redisPoolFactory().getResource();
} catch (Exception e) {
e.printStackTrace();
}
String lockValue = jedis.get(lockKey);
if(value.equals(lockValue)){
jedis.del(lockKey);
}
}
}
User.java
package com.zhuoli.service.springboot.distributed.repeat.submit.common;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
/**
* @Author: zhuoli
* @Date: 2018/9/6 13:42
* @Description:
*/
@Getter
@Setter
@Builder
public class User {
private Long id;
private String name;
private String description;
}
UserController.java
package com.zhuoli.service.springboot.distributed.repeat.submit.controller;
import com.zhuoli.service.springboot.distributed.repeat.submit.annotation.CacheLock;
import com.zhuoli.service.springboot.distributed.repeat.submit.annotation.CacheParam;
import com.zhuoli.service.springboot.distributed.repeat.submit.service.UserControllerService;
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author: zhuoli
* @Date: 2018/9/6 13:37
* @Description:
*/
@RestController
@RequestMapping("/user")
@AllArgsConstructor
public class UserController {
private UserControllerService userControllerService;
@CacheLock(prefix = "user")
@RequestMapping(value = "/get_user", method = RequestMethod.POST)
public ResponseEntity getUserById(@CacheParam(name = "token") @RequestParam Long id){
return ResponseEntity.status(HttpStatus.OK).body(userControllerService.getUserById(id));
}
}
UserControllerService.java
package com.zhuoli.service.springboot.distributed.repeat.submit.service;
import com.zhuoli.service.springboot.distributed.repeat.submit.common.User;
/**
* @Author: zhuoli
* @Date: 2018/9/6 13:43
* @Description:
*/
public interface UserControllerService {
User getUserById(Long id);
}
UserControllerServiceImpl.java
package com.zhuoli.service.springboot.distributed.repeat.submit.service.impl;
import com.google.common.collect.Lists;
import com.zhuoli.service.springboot.distributed.repeat.submit.common.User;
import com.zhuoli.service.springboot.distributed.repeat.submit.service.UserControllerService;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* @Author: zhuoli
* @Date: 2018/9/6 13:44
* @Description:
*/
@Service
public class UserControllerServiceImpl implements UserControllerService {
private static final List<User> userList = Lists.newArrayList();
@PostConstruct
public void initUserList(){
userList.add(User.builder().id(1L).name("zhuoli0").description("zhuoli0 is a programmer").build());
userList.add(User.builder().id(2L).name("zhuoli1").description("zhuoli1 is a programmer").build());
userList.add(User.builder().id(3L).name("zhuoli2").description("zhuoli2 is a programmer").build());
}
@Override
public User getUserById(Long id) {
return Optional.ofNullable(userList.stream().filter(user -> user.getId().equals(id)).collect(Collectors.toList())).map(list -> list.get(0)).orElse(null);
}
}
参考资料
https://blog.csdn.net/weixin_41835612/article/details/83738244