概念:
一次和多次請求某一個資源悦屏,對資源本身所產(chǎn)生的的影響均與一次執(zhí)行的影響相同; 需要考慮到冪等的場景就是新增和更新三痰,對于查詢,刪除的操作執(zhí)行一次和執(zhí)行多次不影響最終結(jié)果集所以Select, delete 天生就是冪等的
常見解決方案
1:分布式鎖保證冪等
1.1 場景:秒殺窜管,搶票
在秒殺情況下同一個用戶只能搶購一件商品,在買票的情況下同一個用戶稚机,同一個班次的火車票也能買一張幕帆;如果此時用戶是寫的腳本程序或者網(wǎng)絡抖動發(fā)生重試,執(zhí)行了多次request 則會導致一個用戶搶購多個商品或者出票重復
1.2 解決方案:redis 鎖
用戶ID + 火車班次號 生成一個唯一key 存儲到redis中使用setnx / rédission 保證某一段時間內(nèi)的數(shù)據(jù)唯一性
1.3 DEMO 代碼
redis設置全局鎖代碼
public ResponseCode setLockAndExecute(String key, String value, long timeOut, TimeUnit time, Supplier<ResponseCode> execute) {
boolean lock = false;
try {
lock = stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeOut, time);
if (lock) {
log.info(“重復請求:{}”, key);
return execute.get();
}
//加鎖失敗
log.info("重復請求:{}", key);
return ResponseCode.REPEAT_ERROR;
} catch (Exception ex) {
log.error("Redis加鎖異常", ex);
return ResponseCode.UNKNOWN_ERROR;
} finally {
if (lock) {
log.info("任務執(zhí)行完成:刪除鎖:{}",key);
this.del(key);
}
}
}
需要做冪等的業(yè)務方法
return setLockAndExecute(userId+productId, 1, 1, TimeUnit.HOURS, () -> {
User user = userService.getUserById(userId);
if (ObjectUtils.isEmpty(user)) {
log.info(“不存在該用戶:{}”, userid);
return ResponseCode.USER_NOT_FIND;
}
Product product = productService.getProductById(productId);
if (ObjectUtils.isEmpty(product)) {
log.info(“不存在該商品:{}”, productid);
return ResponseCode.PRODUCT_NOT_FIND;
}
String orderId = orderService.insertNewOrder(user, product);
if (StringUtils.hasLength(orderId)) {
log.info(“訂單生成成功:{}”, orderId);
return ResponseCode.SUCCESS;
}
log.info("訂單生成失敗);
return ResponseCode.FAIL;
});
2:token保證冪等
2.1 場景:用戶注冊赖条,創(chuàng)建子部門
當有創(chuàng)建新數(shù)據(jù)的時候因為都是新數(shù)據(jù)沒有ID失乾,當用戶點擊多次提交會導致創(chuàng)建重復數(shù)據(jù)到DB中
2.2 解決方案:在正式請求前做一次獲取令牌的預請求
當點擊新增,或者注冊的時候先請求一次后端接口后端接口生成一個token保存到redis中(setnx)同時返回給前端請求纬乍,等前端填寫完注冊信息并提交保存的時候攜帶上上次返回的token, 后端收到請求后獲取token并且判斷redis中是否存在,如果存在從redis中刪除然后保存數(shù)據(jù)庫碱茁,如果不存在返回錯誤信息給前端
2.3 DEMO 代碼:
創(chuàng)建注冊用戶的冪等攔截注解,以后對于需要冪等的方法只需要在方法上添加該注解即可
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RegisterRestriction {
boolean value() default true;
}
在注冊用戶的controller 上添加冪等判斷注解
@PostMapping(“/register")
@RegisterRestriction
public ResponseData registerUser(@RequestHeader(value = "Authorization") String token, HttpServletResponse response) {
return userService.registerUser(token, response);
}
生成正式請求的令牌代碼
Public ResponseCode generateUniqueKey(String userId) {
User user = userService.getUserById(userId);
if (ObjectUtils.isEmpty(user)) {
log.info(“不存在該用戶:{}”, userid);
return ResponseCode.USER_NOT_FIND;
}
//生成唯一key 給response
String uniqueKey = String.*format*("%s%s", Instant.*now*().toEpochMilli(), userId);
stringRedisTemplate.opsForValue().set(uniqueKey, uniqueKey, 5 * 60, TimeUnit.SECONDS);
response.setHeader(CommonConst.*REGISTER_REQUEST_ID*, uniqueKey);
// 成功更新仿贬,返回
return ResponseCode.SUCCESS;
}
冪等攔截器代碼
@Slf4j
public class RegisterInterceptor implements HandlerInterceptor {
@Autowired
private RedisUtils redisUtils;
private boolean checkRequestId(HttpServletRequest request, HttpServletResponse response) throws InterceptorException {
String requestId = request.getHeader("X-Request-Id");
if (!StringUtils.hasLength(requestId)) {
log.info("header中未獲取到預先生成的注冊令牌纽竣,請求非法");
throw new InterceptorException(40012, "header中未獲取到預先生成的注冊令牌,請求非法");
}
//判斷redis中是否存在requestId, 如果存在 -> return true
//如果不存在(可能是前端造的假ID-> return false)
if (redisUtils.exists(requestId)) {
//刪除
if (redisUtils.del(requestId)) {
return true;
}
log.info("請勿重復請求");
throw new InterceptorException(ResponseCode.REPEAT_ERROR);
}
log.info("redis 中未獲取到預先生成的注冊令牌茧泪,請求非法");
throw new InterceptorException(ResponseCode.REPEAT_ERROR);
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod)handler;
Method method = handlerMethod.getMethod();
if (!ObjectUtils.isEmpty(method) && method.isAnnotationPresent(RegisterRestriction.class)) {
if (method.getAnnotation(RegisterRestriction.class).value()) {
return checkRequestId(request, response);
}
}
}
return true;
}
}
3:mysql unique key 保證冪等
3.1 解決方案:
通過對mysql 表創(chuàng)建唯一key來達到冪等性蜓氨,比如用戶注冊手機號設置唯一key 當發(fā)生重復請求后mysql判重拒絕寫入db,
創(chuàng)建子部門,可以創(chuàng)建parent_id + 部門名稱做唯一key 來保證冪等性
3.3 DEMO 代碼:
ALTER TABLE tb_department ADD CONSTRAINT department_info_UN UNIQUE KEY (parent_id,department_name);
4:樂觀鎖保證冪等
4.1 場景:
當涉及到自增队伟,自減更新的時候需要考慮冪等性問題穴吹,因為普通的更新就是給字段賦值執(zhí)行一次和執(zhí)行多次效果都是一樣的,比如:更新用戶名稱嗜侮,更新數(shù)據(jù)狀態(tài)
Update user set name= ‘張三’ where id = ‘zhangsan’
Update user set status=‘1’ where id = ‘zhansan’
4.2 解決方案:樂觀鎖
對于自增自減類的update,比如:update goods set count=count -1 where id = ‘123’ 就需要做冪等處理因為執(zhí)行多次就會自減多次港令,使用樂觀鎖啥容,給表中新增一個version字段,每次讀取的時候?qū)ersion字段讀取顷霹,更新的時候再判斷version字段是否是自己持有的值同時將version +1
Update goods set count=count -1 , version=version +1 where id = ‘123’ and version = ‘1’