Java 分布式服务重复提交解决方案 Redis
本文实现一种分布式服务防重复提交的方案 也就是一线一个锁,在方法请求前,要先获取锁 如果锁存在则返回异常 。
下面简单介绍一下如何使用Redis实现分布式锁
- CacheLock.java 为自定义注解接口,CacheLock方法注解用来指定分布式锁的key前缀和失效时间等信息
- LockKeyGenerator.java为切面,用于拦截Heders中token参数,生成分布式锁的key
- LockMethodInterceptor.java为切面,用于拦截@CacheLock方法,实现在执行方法之前要先获取锁逻辑
- RedisLockHelper.java为分布式锁的实现
1.pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.service</groupId><artifactId>springboot-repeat-submit</artifactId><version>1.0-SNAPSHOT</version><!-- Spring Boot 启动父依赖 --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.3.RELEASE</version></parent><dependencies><!-- Exclude Spring Boot's Default Logging --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- https://mvnrepository.com/artifact/redis.clients/jedis --><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.9.0</version></dependency><!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.2</version><scope>provided</scope></dependency><dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>21.0</version></dependency></dependencies></project>
2.自定义注解
@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 ":";
}
3.分布式锁key 生成
public class LockKeyGenerator implements CacheKeyGenerator {@Overridepublic String getLockKey(ProceedingJoinPoint pjp) {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();CacheLock lockAnnotation = method.getAnnotation(CacheLock.class);StringBuilder builder = new StringBuilder();HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader(HttpHeaders.AUTHORIZATION);builder.append(lockAnnotation.delimiter()).append(token);return lockAnnotation.prefix() + builder.toString();}
}
如果想要使用请求参数中的属性生成分布式key
可以添加自定义注解 CacheParam.java 使用@CacheParam参数生产key
@CacheParam需要作用在请求参数上
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {/*** 字段名称** @return String*/String name() default "";
}
public class LockKeyGenerator implements CacheKeyGenerator {@Overridepublic 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();}
}
集成token
如果没有token 方法上添加@RequestParam @RequestBody注解 可在方法上添加@CacheParam(name=“对应的字段名”)
public class LockKeyGenerator implements CacheKeyGenerator {@Overridepublic String getLockKey(ProceedingJoinPoint pjp) {CacheLock lockAnnotation = null;StringBuilder builder = null;try {MethodSignature signature = (MethodSignature) pjp.getSignature();Method method = signature.getMethod();lockAnnotation = method.getAnnotation(CacheLock.class);PassLogin passLoginAnnotation = method.getAnnotation(PassLogin.class);builder = new StringBuilder();if(passLoginAnnotation == null){HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();String token = request.getHeader(HttpHeaders.AUTHORIZATION);builder.append(lockAnnotation.delimiter()).append(token);} else {final Object[] args = pjp.getArgs();final Parameter[] parameters = method.getParameters();//默认解析方法里面带CacheParam注解的属性,如果没有尝试着解析实体对象中的CacheParam注解属性for (int i = 0; i < parameters.length; i++) {final CacheParam annotation = parameters[i].getAnnotation(CacheParam.class);if (annotation == null) {continue;} else {final Object object = args[i];if(object instanceof String){builder.append(lockAnnotation.delimiter()).append(args[i]);} else {final Field[] fields = object.getClass().getDeclaredFields();if(fields != null){for (Field field : fields) {if(field.getName().equals(annotation.name())){field.setAccessible(true);builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field, object));break;}}}}}}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));}}}}} catch (Exception e) {e.printStackTrace();}return lockAnnotation.prefix() + builder.toString();}
4.分布式锁实现
@Configuration
public class RedisConfig {@Beanpublic RedisConnectionFactory redisConnectionFactory() {RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration("127.0.0.1", 6379);return new JedisConnectionFactory(redisStandaloneConfiguration);}@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(redisConnectionFactory);template.setKeySerializer(new StringRedisSerializer());template.setValueSerializer(new GenericJackson2JsonRedisSerializer());return template;}}
@Configuration
@RequiredArgsConstructor
public class RedisLockHelper {private static final String DELIMITER = "|";private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);private final StringRedisTemplate stringRedisTemplate;/*** 获取锁* @param lockKey lockKey* @param uuid UUID* @param timeout 超时时间* @param unit 过期单位* @return true or false*/public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);if (success) {/*设置过期时间,防止系统崩溃而导致锁迟迟不释放形成死锁*/stringRedisTemplate.expire(lockKey, timeout, unit);} else {String oldVal = stringRedisTemplate.opsForValue().get(lockKey);final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));/*缓存已经到过期时间,但是还没释放,避免ddl失效造成死锁*/if (Long.parseLong(oldValues[0]) + unit.toSeconds(1) <= System.currentTimeMillis()) {stringRedisTemplate.opsForValue().set(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);stringRedisTemplate.expire(lockKey, timeout, unit);return true;}}return success;}public void unlock(String lockKey, String value) {unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);}/*** 延迟unlock** @param lockKey key* @param uuid* @param delayTime 延迟时间* @param unit 时间单位*/private void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {if (StringUtils.isEmpty(lockKey)) {return;}if (delayTime <= 0) {doUnlock(lockKey, uuid);} else {/*定时任务延迟unlock*/EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);}}/*** @param lockKey key* @param uuid*/private void doUnlock(final String lockKey, final String uuid) {String val = stringRedisTemplate.opsForValue().get(lockKey);final String[] values = val.split(Pattern.quote(DELIMITER));if (values.length <= 0) {return;}if (uuid.equals(values[1])) {stringRedisTemplate.delete(lockKey);}}}
简单讲一下锁的实现,Redis是线程安全的,利用该的特性可以很轻松的实现一个分布式锁。opsForValue().setIfAbsent(key,value)的作用是如果缓存中没有当前Key则进行缓存同时返回true,否则返回false。只靠这一个逻辑其实也算是实现了锁,但是为了防止防止系统崩溃而导致锁迟迟不释放形成死锁,或者Redis ddl失效导致死锁,又添加一些比如key失效时间等逻辑。可以仔细读一下,并不难理解。
5.分布式锁切面
拦截@CacheLock注解方法,在方法执行前增加获取锁逻辑
@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);}}
}
6.注解使用
该方法使用token生成key
key = prefix + “:” + token
@CacheLock(prefix = "checkDeviceNo")@RequestMapping(value = "/checkDeviceNo", method = {RequestMethod.POST})public R checkDeviceNo(@RequestParam String deviceNo) {Integer countDeviceNo = cameraDeviceService.countDeviceNo(deviceNo);if (countDeviceNo != null && countDeviceNo > 0) {return R.failed(ErrorCodeEnum.PB10010001.msg());} else {return R.ok();}}
使用请求参数生成key
key = prefix + “:” + deviceNo
@CacheLock(prefix = "checkDeviceNo")@RequestMapping(value = "/checkDeviceNo", method = {RequestMethod.POST})public R checkDeviceNo(@CacheParam(name = "deviceNo") @RequestParam String deviceNo) {Integer countDeviceNo = cameraDeviceService.countDeviceNo(deviceNo);if (countDeviceNo != null && countDeviceNo > 0) {return R.failed(ErrorCodeEnum.PB10010001.msg());} else {return R.ok();}}
以上就是实现Java分布式锁重复提交问题的解决方案,欢迎提更好的解决方案!!!
Java 分布式服务重复提交解决方案 Redis相关推荐
- 接口重复提交解决方案
接口重复提交解决方案 参考文章: (1)接口重复提交解决方案 (2)https://www.cnblogs.com/java-le/p/11056635.html (3)https://www.cod ...
- Java的token解决方案,SpringMVC后台token防重复提交解决方案
思路1.添加拦截器,拦截需要防重复提交的请求 2.通过注解@Token来添加token/移除token 3.前端页面表单添加(如果是Ajax请求则需要在请求的json数据中添加token值) 核 ...
- java分布式事务——seata,tcc解决方案总结!
目录 1.分布式事务基础理论 1.1.CAP理论 1.2.BASE理论 2.分布式事务解决方案之2PC(两阶段提交) 2.2.1 XA方案 2.2.2 Seata方案 2.2.3分布式事务解决方案之T ...
- java mysql防重复提交_防止数据重复提交的6种方法(超简单)!
有位朋友,某天突然问磊哥:在 Java 中,防止重复提交最简单的方案是什么? 这句话中包含了两个关键信息,第一:防止重复提交:第二:最简单. 于是磊哥问他,是单机环境还是分布式环境? 得到的反馈是单机 ...
- Java实现防重复提交
欢迎访问我的个人博客:www.ifueen.com 防重复提交的重要性? 在业务开发中,为什么我们要去想办法解决重复提交这一问题发生?网上的概念很多:导致表单重复提交,造成数据重复,增加服务器负载,严 ...
- java后端 防重复提交_后台防止表单重复提交
具体的做法: 1.获取用户填写用户名和密码的页面时向后台发送一次请求,这时后台会生成唯一的随机标识号,专业术语称为Token(令牌). 2.将Token发送到客户端的Form表单中,在Form表单中使 ...
- java mvc中重复提交表单,spring mvc 防止重复提交表单的两种方法,推荐第二种
第一种方法:判断session中保存的token 比较麻烦,每次在提交表单时都必须传入上次的token.而且当一个页面使用ajax时,多个表单提交就会有问题. 注解Token代码: package c ...
- java mvc中重复提交_SpringMVC之——防止重复提交表单的方法(一)
这篇博文介绍第一种方法:判断session或其他缓存中保存的token,这里以session为例,具体大家也可以自行扩展以其他的缓存实现. 这种方式比较麻烦,每次在提交表单时都必须传入上次的token ...
- java表单重复提交_JavaWeb防止表单重复提交(转载)
转载自:http://blog.csdn.net/ye1992/article/details/42873219 在平时开发中,如果网速比较慢的情况下,用户提交表单后,发现服务器半天都没有响应,那么用 ...
- java分布式服务框架Dubbo的介绍与使用
1. Dubbo是什么? Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案.简单的说,dubbo就是个服务框架,如果没有分布式的需求,其实是不需 ...
最新文章
- Zabbix 3.0 从入门到精通(zabbix使用详解)
- 财务 计算机网络,计算机网络技术在财务系统中的应用
- JZOJ 3731. 【NOIP2014模拟7.10】庐州月
- mysql_常用命令
- .NET 4.5 Task异步编程学习资料
- 性#26684;倔强的HTML5员工
- 编程不仅是写代码!?
- nginx的高级配置(1)——为某个虚拟主机添加用户验证
- java逐行读取文件内容执行sql语句_[11/100] 文件和异常
- OpenCV 直方图均衡化 equalizeHist
- ubuntu20.04安装谷歌拼音输入法
- 奥克兰计算机科学专业世界排名,2020年新西兰计算机科学专业大学排名
- 一些关于罗马字符的知识
- 图/图的存储/图的遍历
- java 程序怎么设置中文_怎么让这个简单JAVA程序读写中文字符
- java web属于什么语言_java web开发是什么
- 三星 android 自定义物理按键,新机皇驾到!三星Note10将取消物理按键,网友:这造型逼死强迫症...
- [卓意听书]6月感恩活动,Q币送不停!
- Why Python is Slow? Looking Under the Hood
- b2b、c2c、B2C、B2B2C分别是什么意思?有什么区别?