Spring Boot + Redis實戰-利用自定義注解+分布式鎖實現接口冪等性


場景

不管是傳統行業還是互聯網行業,我們都需要保證大部分操作是冪等性的,簡單點說,就是無論用戶點擊多少次,操作多少遍,產生的結果都是一樣的,是唯一的。而今次公司的項目里,又被我遇到了這么一個冪等性的問題,就是用戶的余額充值、創建訂單和訂單支付,不管用戶點擊多少次,只會有一條充值記錄,一條新訂單記錄,一條訂單支付記錄。

技術方案

現在使用比較廣泛的方案都是基於Redis。
方案:Redis+token

  • 處理流程:數據提交前,前端要向服務端的申請token,token(帶有過期時間)放到redis;當數據提交時帶上token,如果刪除token成功則表明token未過期,然后進行業務邏輯,否則就是token已過期,提示前端請勿重復提交數據。

而我將使用不同的方案。因為此時前后端對接已走一半,不想讓前端再增加請求token的接口(畢竟后端能搞定的,還是別麻煩前端同學了)。
方案:自定義注解+分布式鎖

  • 處理流程:將需要冪等性的接口加上自定義注解。然后編寫一個切面,在around方法里邏輯:嘗試獲取分布式鎖(帶過期時間),成功表明沒重復提交,否則就是重復提交了。

講解開始

1、添加Redis依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、自定義注解:

/**
 * @author Howinfun
 * @desc 自定義注解:分布式鎖
 * @date 2019/11/12
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheLock {

    /** key前綴 */
    String prefix() default "";

    /** 過期秒數,默認為5秒 */
    int expire() default 5;

    /** 超時時間單位,默認為秒 */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /** Key的分隔符(默認 :)  */
    String delimiter() default ":";
}

3、自定義切面:

/**
 * @author Howinfun
 * @desc 自定義切面
 * @date 2019/11/12
 */
@Aspect
@Component
public class LockCheckAspect {

    /** lua */
    private static final String RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 增強帶有CacheLock注解的方法
    @Pointcut("@annotation(cn.gdmcmc.system.api.config.aop.CacheLock)")
    public void pointCut(){}

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable{

       // 可以根據業務獲取用戶唯一的個人信息,例如手機號碼
       String phone = .....;
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);
        String prefix = cacheLock.prefix();
        if (StringUtils.isBlank(prefix)){
            throw new GlobalException("CacheLock prefix can't be null");
        }
        // 拼接 key
        String delimiter = cacheLock.delimiter();
        StringBuilder sb = new StringBuilder();
        sb.append(prefix).append(delimiter).append(phone);
        final String lockKey = sb.toString();
        final String UUID = cn.hutool.core.lang.UUID.fastUUID().toString();
        try {
            // 獲取鎖
            boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey,UUID,cacheLock.expire(),cacheLock.timeUnit());
            if (!success){
                throw new CustomDeniedException("請勿重復提交");
            }
            Object result= joinPoint.proceed();
            return result;
        }finally {
            // 最后記得釋放鎖
            DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA_SCRIPT,Long.class);
            Long result = redisTemplate.execute(redisScript, Collections.singletonList(lockKey),UUID);
        }

    }
}

4、到此,只要為需要保證冪等性的接口上加上@CacheLock注解,就可以了。

@RestController
@RequestMapping(value = "/charge")
@AllArgsConstructor
public class ChargeController {    
    
    @PostMapping("/startCharge")
    @CacheLock(prefix = "recharge")
    public Result startCharge(@RequestBody @Validated({ChargeQuery.QRCodeNotBlank.class}) ChargeQuery query){
        return this.chargeChargeService.startCharge(query);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM