之前博文中介紹過token 機制處理 接口冪等性問題,這種方式一個問題對代碼的入侵比較多,
相對書寫代碼來講就比較麻煩,本文介紹使用 redis 分布式鎖機制解決接口冪等性問題。
1:定義注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Ide{ /** * 設置請求鎖定時間,超時后自動釋放鎖 * * @return */ int lockTime() default 10; }
2:AOP 實現 注解 @Ide
的攔截處理
/** * 接口冪等性的 -- 分布式鎖實現 */ @Slf4j @Aspect @Component public class ReqSubmitAspect { @Autowired private RedisLock redisLock; @Pointcut("@annotation(com.laiease.common.annotation.Ide)") public void idePointCut() { } @Around("idePointCut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 使用分布式鎖 機制-實現 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Ide ide = method.getAnnotation(Ide.class); int lockSeconds = ide.lockTime(); HttpServletRequest request = HttpContextUtils.getHttpServletRequest(); AssertUtils.notNull(request, "request can not null"); // 獲取請求的憑證,本項目中使用的JWT,可對應修改 String token = request.getHeader("Token"); String requestURI = request.getRequestURI(); String key = getIdeKey(token, requestURI); String clientId = CmUtil.getUUID(); // 獲取鎖 boolean lock = redisLock.tryLock(key, clientId, lockSeconds); log.info("tryLock key = [{}], clientId = [{}]", key, clientId); if (lock) { log.info("tryLock success, key = [{}], clientId = [{}]", key, clientId); // 獲取鎖成功 Object result; try { // 執行進程 result = joinPoint.proceed(); } finally { // 解鎖 redisLock.releaseLock(key, clientId); log.info("releaseLock success, key = [{}], clientId = [{}]", key, clientId); } return result; } else { // 獲取鎖失敗,認為是重復提交的請求 log.info("tryLock fail, key = [{}]", key); throw new RuntimeException("重復請求,請稍后再試!"); } } private String getIdeKey(String token, String requestURI) { return token + requestURI; } }
3:redis 分布式鎖工具類
@Component public class RedisLock { private static final Long RELEASE_SUCCESS = 1L; private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; // 當前設置 過期時間單位, EX = seconds; PX = milliseconds private static final String SET_WITH_EXPIRE_TIME = "EX"; //lua private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; @Autowired private StringRedisTemplate redisTemplate; /** * 該加鎖方法僅針對單實例 Redis 可實現分布式加鎖 * 對於 Redis 集群則無法使用 * <p> * 支持重復,線程安全 * * @param lockKey 加鎖鍵 * @param clientId 加鎖客戶端唯一標識(采用UUID) * @param seconds 鎖過期時間 * @return */ public boolean tryLock(String lockKey, String clientId, long seconds) { return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { // Jedis jedis = (Jedis) redisConnection.getNativeConnection(); Object nativeConnection = redisConnection.getNativeConnection(); RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer(); byte[] keyByte = stringRedisSerializer.serialize(lockKey); byte[] valueByte = stringRedisSerializer.serialize(clientId); // lettuce連接包下 redis 單機模式 if (nativeConnection instanceof RedisAsyncCommands) { RedisAsyncCommands connection = (RedisAsyncCommands) nativeConnection; RedisCommands commands = connection.getStatefulConnection().sync(); String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds)); if (LOCK_SUCCESS.equals(result)) { return true; } } // lettuce連接包下 redis 集群模式 if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) { RedisAdvancedClusterAsyncCommands connection = (RedisAdvancedClusterAsyncCommands) nativeConnection; RedisAdvancedClusterCommands commands = connection.getStatefulConnection().sync(); String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds)); if (LOCK_SUCCESS.equals(result)) { return true; } } if (nativeConnection instanceof JedisCommands) { JedisCommands jedis = (JedisCommands) nativeConnection; String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds); if (LOCK_SUCCESS.equals(result)) { return true; } } return false; }); } /** * 與 tryLock 相對應,用作釋放鎖 * * @param lockKey * @param clientId * @return */ public boolean releaseLock(String lockKey, String clientId) { DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>(); redisScript.setScriptText(RELEASE_LOCK_SCRIPT); redisScript.setResultType(Integer.class); // Integer execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), clientId); Object execute = redisTemplate.execute((RedisConnection connection) -> connection.eval( RELEASE_LOCK_SCRIPT.getBytes(), ReturnType.INTEGER, 1, lockKey.getBytes(), clientId.getBytes())); if (RELEASE_SUCCESS.equals(execute)) { return true; } return false; } }