
1.什么是幂等

在我们编程中常见幂等
-
select查询天然幂等
-
delete删除也是幂等,删除同一个多次效果一样
-
update直接更新某个值的,幂等
-
update更新累加操作的,非幂等
-
insert非幂等操作,每次新增一条
2.产生原因
-
点击提交按钮两次;
-
点击刷新按钮;
-
使用浏览器后退按钮重复之前的操作,导致重复提交表单;
-
使用浏览器历史记录重复提交表单;
-
浏览器重复的HTTP请;
-
nginx重发等情况;
-
分布式RPC的try重发等;
3.解决方案
①配置注解
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface Resubmit {/*** 延时时间 在延时多久后可以再次提交** @return Time unit is one second*/int delaySeconds() default 20;}
②实例化锁
/*** @author lijing* 重复提交锁*/@Slf4jpublic final class ResubmitLock {private static final ConcurrentHashMap LOCK_CACHE = new ConcurrentHashMap<>(200);private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy());// private static final Cache CACHES = CacheBuilder.newBuilder()// 最大缓存 100 个// .maximumSize(1000)// 设置写缓存后 5 秒钟过期// .expireAfterWrite(5, TimeUnit.SECONDS)// .build();private ResubmitLock() {}/*** 静态内部类 单例模式** @return*/private static class SingletonInstance {private static final ResubmitLock INSTANCE = new ResubmitLock();}public static ResubmitLock getInstance() {return SingletonInstance.INSTANCE;}public static String handleKey(String param) {return DigestUtils.md5Hex(param == null ? "" : param);}/*** 加锁 putIfAbsent 是原子操作保证线程安全** @param key 对应的key* @param value* @return*/public boolean lock(final String key, Object value) {return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value));}/*** 延时释放锁 用以控制短时间内的重复提交** @param lock 是否需要解锁* @param key 对应的key* @param delaySeconds 延时时间*/public void unLock(final boolean lock, final String key, final int delaySeconds) {if (lock) {EXECUTOR.schedule(() -> {LOCK_CACHE.remove(key);}, delaySeconds, TimeUnit.SECONDS);}}}
③AOP 切面
@Log4j@Aspect@Componentpublic class ResubmitDataAspect {private final static String DATA = "data";private final static Object PRESENT = new Object();@Around("@annotation(com.cn.xxx.common.annotation.Resubmit)")public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable {Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();//获取注解信息Resubmit annotation = method.getAnnotation(Resubmit.class);int delaySeconds = annotation.delaySeconds();Object[] pointArgs = joinPoint.getArgs();String key = "";//获取第一个参数Object firstParam = pointArgs[0];if (firstParam instanceof RequestDTO) {//解析参数JSONObject requestDTO = JSONObject.parseObject(firstParam.toString());JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA));if (data != null) {StringBuffer sb = new StringBuffer();data.forEach((k, v) -> {sb.append(v);});//生成加密参数 使用了content_MD5的加密方式key = ResubmitLock.handleKey(sb.toString());}}//执行锁boolean lock = false;try {//设置解锁keylock = ResubmitLock.getInstance().lock(key, PRESENT);if (lock) {//放行return joinPoint.proceed();} else {//响应重复提交异常return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION);}} finally {//设置解锁key和解锁时间ResubmitLock.getInstance().unLock(lock, key, delaySeconds);}}}
④注解使用案例
@ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口")@PostMapping("/posts/save")@Resubmit(delaySeconds = 10)public ResponseDTO saveBbsPosts(@RequestBody @Validated RequestDTO requestDto) {return bbsPostsBizService.saveBbsPosts(requestDto);}
<dependencies><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-webartifactId>dependency><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-aopartifactId>dependency><dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-data-redisartifactId>dependency>dependencies>
spring.redis.host=localhostspring.redis.port=6379spring.redis.password=123456
@Aspect@Configurationpublic class LockMethodInterceptor {@Autowiredpublic LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) {this.redisLockHelper = redisLockHelper;this.cacheKeyGenerator = cacheKeyGenerator;}private final RedisLockHelper redisLockHelper;private final CacheKeyGenerator cacheKeyGenerator;@Around("execution(public * *(..)) && @annotation(com.battcn.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 {// 假设上锁成功,但是设置过期时间失效,以后拿到的都是 falsefinal 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 {// TODO 如果演示的话需要注释该代码;实际应该放开redisLockHelper.unlock(lockKey, value);}}}
@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)public class RedisLockHelper {private static final String DELIMITER = "|";/*** 如果要求比较高可以通过注入的方式分配*/private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);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<Boolean>) 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();boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);if (success) {stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS);} else {String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {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);}}}
https://blog.battcn.com/2018/06/13/springboot/v2-cache-redislock/
本文分享自微信公众号 - Java后端(web_resource)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
相关文章
暂无评论...
