1.RepeatAnnotation.java
package com.repeat.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RepeatAnnotation{
/**
* redis 锁key的前缀
*
* @return redis 锁key的前缀
*/
String prefix() default "";
/**
* 过期秒数,默认为10秒
*
* @return 轮询锁的时间
*/
int expire() default 10;
/**
* 超时时间单位
*
* @return 秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* Key的分隔符(默认 :)
* 生成的Key:N:SO1008:500
*
* @return String
*/
String delimiter() default "_";
}
2.RepeatInterceptor.java
类中注释掉的代码替换对应功能的代码后几位全局请求进行拦截
package com.battcn.interceptor;
import com.battcn.annotation.CacheLock;
import com.battcn.utils.RedisLockHelper;
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.util.StringUtils;
import java.lang.reflect.Method;
import java.util.UUID;
/**
* 重复提交实现
*
*/
@Aspect
@Component
public class RepeatInterceptor{
@Autowired
public RepeatInterceptor(RedisLockHelper redisLockHelper) {
this.redisLockHelper = redisLockHelper;
}
private final RedisLockHelper redisLockHelper;
@Around("execution(public * *(..)) && @annotation(com.repeat.annotation.RepeatAnnotation)")
// @Around("execution(public * com.battcn.controller.*.*(..))")
public Object interceptor(ProceedingJoinPoint pjp) {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
RepeatAnnotation lock = method.getAnnotation(RepeatAnnotation.class);
if (StringUtils.isEmpty(lock.prefix())) {
throw new RuntimeException("lock key don't null...");
}
// 此处需要获取当前登录用户的有效token值
String token = "45afwea556614511";
final String lockKey = lock.prefix() + lock.delimiter() + token;
// final String lockKey = "test_" + token;
System.out.println("lockKeylockKeylockKey>>>"+lockKey);
String value = UUID.randomUUID().toString();
try {
// 假设上锁成功,但是设置过期时间失效,以后拿到的都是 false
final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit());
// final boolean success = redisLockHelper.lock(lockKey, value, 10, TimeUnit.SECONDS);
if (!success) {// 返回false时,说明此时缓存仍在有效期内,直接返回重复提交
return "重复提交";
}
// 返回true时
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("系统异常");
}
} finally {
// 如果测试需要注释该代码;实际运行时应该放开,删除对应缓存
// redisLockHelper.unlock(lockKey, value);
}
}
}
3.RedisLockHelper.java
package com.repeat.utils;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
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.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
/**
* 需要定义成 Bean
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLockHelper {
private static final String DELIMITER = "|";
/**
* 如果要求比较高可以通过注入的方式分配
*/
// 线程,任务调度,保证并发线程安全
private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(100);
// StringRedisTemplate继承RedisTemplate,两者数据不共通,单独管理,如果存取的数据类型确定为字符串就用这个
private final StringRedisTemplate stringRedisTemplate;
public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
/**
* 获取锁(存在死锁风险)
*
* @param lockKey lockKey
* @param value value
* @param time 超时时间
* @param unit 过期单位
* @return true or false
*/
public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
return stringRedisTemplate.execute((RedisCallback) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
}
/**
* 获取锁
*
* @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();
// 设置缓存 如果键不存在则新增,存在则不改变已经有的值。因为不删除所以设置会在第一次设置后变为false
boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
System.out.println("缓存设置::::"+success);
if (success) {// 如果缓存设置成功,则给其设置过期时间
stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);//设置过期时间
} else {
//获取原来key键对应的值并重新赋新值。
String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
// 拆分旧值
final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
// 如果旧值加+1 小于等于 当前系统时间 返回true 否则返回添加缓存状态false
if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
System.out.println("旧值小于 当前系统时间-1");
return true;
}
}
return success;
}
/**
* @see Redis Documentation: SET
*/
public void unlock(String lockKey, String value) {
unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
}
/**
* 延迟unlock
*
* @param lockKey key
* @param uuid client(最好是唯一键的)
* @param delayTime 延迟时间
* @param unit 时间单位
*/
public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
if (StringUtils.isEmpty(lockKey)) {
return;
}
if (delayTime <= 0) {
doUnlock(lockKey, uuid);
} else {
EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
}
}
/**
* @param lockKey key
* @param uuid client(最好是唯一键的)
*/
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);
}
}
}
4.TestController.java
package com.repeat.controller;
import com.battcn.annotation.CacheLock;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestController {
@CacheLock(prefix = "test")
@GetMapping
public String query() {
return "提交成功";
}
}
5.pom.xml 所需依赖
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.0.2.RELEASE
com.battcn
chapter22
0.0.1-SNAPSHOT
jar
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
org.springframework.boot
spring-boot-starter-data-redis
io.springfox
springfox-swagger2
2.7.0
io.springfox
springfox-swagger-ui
2.7.0
org.springframework.boot
spring-boot-maven-plugin