電子商務網站開發(fā)教程書內代碼推廣普通話宣傳周
用戶鎖的作用
秒殺、支付等場景,用戶頻繁點擊按鈕,會造成同一時刻調用多次接口【第一次請求接口還沒響應數(shù)據(jù),用戶又進行了第二次請求】,造成數(shù)據(jù)異常和網絡擁堵。添加用戶鎖,在用戶第二次點擊按鈕時,攔擊用戶請求。限制用戶在操作未完成前不能再進行下一次相同操作
1.主要組成
Dul
:用戶鎖注解,自定義鎖注解,然后給需要加鎖的方法加上此注解
DistributedUserLock
:鎖接口
RedisDistributedUserLock
:分布式鎖實現(xiàn)類
DistributedUserLockAspect
:切面類【核心】
自定義鎖注解,利用切面給所有加注解的方法加分布式鎖防止用戶重復點擊按鈕。
2.關鍵代碼分析:
DistributedUserLockAspect
:用redis的setIfAbsent 進行加鎖,防止死鎖,10秒后自動釋放鎖。
@Around("@annotation(com.nascent.ecrp.mall.common.distribute.Dul)")public Object distributedLock(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {if (request == null) {return proceedingJoinPoint.proceed();}String token = request.getHeader("token");if (StringUtils.isBlank(token)) {return proceedingJoinPoint.proceed();}MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();Method method = methodSignature.getMethod();String lockKey = "USER_LOCK_KEY_OF_" + method.getDeclaringClass().getName() + "." + method.getName();Dul distributedKey = method.getAnnotation(Dul.class);int lockTime = distributedKey.lockTimeSeconds();TimeUnit timeUnit = distributedKey.timeUnit();// USER_LOCK_KEY_OF_+類名+方法名+token作為redis的keylockKey += "_" + token;try {// 加鎖成功,說明請求已經處理完,可以繼續(xù)訪問if (distributedUserLock.lock(lockKey, lockTime, timeUnit)) {return proceedingJoinPoint.proceed();} else {// 加鎖失敗,說明第一次的請求還沒處理完throw new WmDefinitelyRuntimeException("操作過于頻繁,請稍后再試");}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new WmDefinitelyRuntimeException(e);} finally {distributedUserLock.unlock(lockKey);}}
3.全部代碼
Dcl
/*** 分布式用戶鎖* 限制用戶在操作未完成前不能再進行下一次相同操作**/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dul {/*** 鎖定的時間防止死鎖,單位秒,默認10** @return 同步鎖定的時間*/int lockTimeSeconds() default 10;/*** 時間單位** @return 時間單位*/TimeUnit timeUnit() default TimeUnit.SECONDS;
}
DistributedUserLock
默認10s后自動釋放鎖
/*** 分布式用戶鎖*/
public interface DistributedUserLock extends Lock {/*** 鎖住用戶操作** @param lockKey 鎖* @param expireSeconds 鎖有效時間* @param timeUnit 時間單位* @return 是否獲取成功* @throws InterruptedException 中斷異常*/boolean lock(String lockKey, int expireSeconds, TimeUnit timeUnit) throws InterruptedException;/*** 釋放分布式鎖** @param lockKey 鎖*/void unlock(String lockKey);
}
RedisDistributedUserLock
/*** redis 分布式鎖*/
@SuppressWarnings({"UnusedReturnValue", "NullableProblems", "unused", "RedundantThrows"})
@Component
public class RedisDistributedUserLock implements DistributedUserLock {/*** 鎖存在*/private final static Long TYPE_LOCK = 1L;/*** 緩存*/@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Overridepublic void lock() {throw new UnsupportedOperationException();}@Overridepublic void lockInterruptibly() throws InterruptedException {throw new InterruptedException();}@Overridepublic boolean tryLock() {throw new UnsupportedOperationException();}/*** 鎖* @param lockKey 鎖* @param expireSeconds 鎖有效時間* @param timeUnit 時間單位* @return true 可以進行操作,false 鎖已存在* @throws InterruptedException*/@Overridepublic boolean lock(String lockKey, int expireSeconds, TimeUnit timeUnit) throws InterruptedException {/* Object obj = redisTemplate.opsForValue().get(lockKey);if (obj != null) {return false;}redisTemplate.opsForValue().set(lockKey, TYPE_LOCK, expireSeconds, timeUnit);return true;*/// 加鎖,如果key不存在,進行加鎖,如果key已經存在返回加鎖失敗return redisTemplate.opsForValue().setIfAbsent(lockKey, TYPE_LOCK, expireSeconds, timeUnit);}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {throw new UnsupportedOperationException();}/*** 解鎖* @param lockKey 鎖*/@Overridepublic void unlock(String lockKey) {redisTemplate.delete(lockKey);}@Overridepublic void unlock() {throw new UnsupportedOperationException();}@SuppressWarnings("NullableProblems")@Overridepublic Condition newCondition() {throw new UnsupportedOperationException();}}
DistributedUserLockAspect
/*** 用戶操作鎖Aspect*/
@Aspect
@Component
@Order(-1)
public class DistributedUserLockAspect {/*** 分布式鎖*/@Autowiredprivate DistributedUserLock distributedUserLock;@Autowiredprivate HttpServletRequest request;/*** @param proceedingJoinPoint proceedingJoinPoint*/@Around("@annotation(com.nascent.ecrp.mall.common.distribute.Dul)")public Object distributedLock(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {if (request == null) {return proceedingJoinPoint.proceed();}String token = request.getHeader("token");if (StringUtils.isBlank(token)) {return proceedingJoinPoint.proceed();}MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();Method method = methodSignature.getMethod();String lockKey = "USER_LOCK_KEY_OF_" + method.getDeclaringClass().getName() + "." + method.getName();Dul distributedKey = method.getAnnotation(Dul.class);int lockTime = distributedKey.lockTimeSeconds();TimeUnit timeUnit = distributedKey.timeUnit();// USER_LOCK_KEY_OF_+類名+方法名+token作為redis的keylockKey += "_" + token;try {// 加鎖成功,說明請求已經處理完,可以繼續(xù)訪問if (distributedUserLock.lock(lockKey, lockTime, timeUnit)) {return proceedingJoinPoint.proceed();} else {// 加鎖失敗,說明第一次的請求還沒處理完throw new WmDefinitelyRuntimeException("操作過于頻繁,請稍后再試");}} catch (InterruptedException e) {Thread.currentThread().interrupt();throw new WmDefinitelyRuntimeException(e);} finally {distributedUserLock.unlock(lockKey);}}
}
秒殺接口添加鎖注解
秒殺活動開始,用戶點擊按鈕,進行上鎖,lockTimeSeconds
【默認10秒】內不能再點擊,lockTimeSeconds
秒后自動釋放鎖。
/*** 參與秒殺** @return CommonResult*/@Dul@Dcl(keyIndex = 0)@Transactional(rollbackFor = Exception.class)@Overridepublic CommonResult doSeckill(WmMarketingSeckillPostVo seckillPostVo) {String checkResult = checkGoodsSeckillInfo(seckillPostVo.getSeckillOrderInfoList(),seckillPostVo.getShopId());if(checkResult.equals(ErrorCode.SECKILL_ACTIVITY_UPDATE.getCode())){return new CommonResult().setFailed().setCode(ErrorCode.SECKILL_ACTIVITY_UPDATE.getCode()).setMsg("活動狀態(tài)已變更,請重新下單");}List<JSONObject> seckillOrderList = new ArrayList<>();//下單校驗List<SecKillBuyInfo> buyInfos = new ArrayList<>();SeckillOrderCheckRequest seckillOrderCheckRequest = new SeckillOrderCheckRequest();seckillOrderCheckRequest.setCustomerId(getCustomerId());seckillOrderCheckRequest.setShopId(seckillPostVo.getShopId());for(WmMarketingSeckillPostVo.SeckillOrderInfo seckillOrderInfo : seckillPostVo.getSeckillOrderInfoList()){SecKillBuyInfo secKillBuyInfo = new SecKillBuyInfo();secKillBuyInfo.setActivityId(seckillOrderInfo.getMarketingGuid());List<SeckillBuyGoodsInfo> seckillBuyGoodsInfos = new ArrayList<>();SeckillBuyGoodsInfo seckillBuyGoodsInfo = new SeckillBuyGoodsInfo();seckillBuyGoodsInfo.setGoodsId(seckillOrderInfo.getGoodsId());seckillBuyGoodsInfo.setGoodsLibId(seckillOrderInfo.getGoodsLibId());List<SeckillBuySkuInfo> seckillBuySkuInfos = new ArrayList<>();SeckillBuySkuInfo seckillBuySkuInfo = new SeckillBuySkuInfo();seckillBuySkuInfo.setGoodsSkuId(StringUtil.isBlank(seckillOrderInfo.getGoodsSkuId()) ? "0" : seckillOrderInfo.getGoodsSkuId());seckillBuySkuInfo.setBuyCount(seckillOrderInfo.getGoodsNum());seckillBuySkuInfos.add(seckillBuySkuInfo);seckillBuyGoodsInfo.setSkuInfos(seckillBuySkuInfos);seckillBuyGoodsInfos.add(seckillBuyGoodsInfo);secKillBuyInfo.setGoodsList(seckillBuyGoodsInfos);buyInfos.add(secKillBuyInfo);}seckillOrderCheckRequest.setBuyInfos(buyInfos);seckillOrderCheckRequest.setGroupId(getGroupId());SeckillOrderCheckResponse seckillOrderCheckResponse = OpenPlatformClient.exec(getGroupId(), seckillOrderCheckRequest);log.info("【調用中臺下單校驗接口響應結果】seckillOrderCheckResponse="+JSON.toJSONString(seckillOrderCheckResponse));if(!seckillOrderCheckResponse.success&&seckillOrderCheckResponse.getCode().equals("50402")){log.info("【秒殺下單已達限購上限】");return new CommonResult().setFailed().setCode("50402").setMsg("已達限購上限");}if(!seckillOrderCheckResponse.success&&seckillOrderCheckResponse.getCode().equals("50404")){log.info("【秒殺剩余庫存不足】");return new CommonResult().setFailed().setCode("50404").setMsg("剩余庫存不足");}if(!seckillOrderCheckResponse.success&&seckillOrderCheckResponse.getCode().equals("50005")){log.info("【秒殺活動已結束】");return new CommonResult().setFailed().setCode("50005").setMsg("活動已結束");}AssertUtil.assertTrue(seckillOrderCheckResponse.getSuccess()&&seckillOrderCheckResponse.getResult().isSeckillSuccess(),"秒殺活動校驗失敗");Map<String,ActivitySeckillCheckInfo> activityCheckInfoMap = new HashMap<>();for(ActivitySeckillCheckInfo activitySeckillCheckInfo : seckillOrderCheckResponse.getResult().getCheckInfos()){Long goodsId = activitySeckillCheckInfo.getItemInfos().get(0).getGoodsId();Long goodsLibId = activitySeckillCheckInfo.getItemInfos().get(0).getGoodsLibId();String skuId = activitySeckillCheckInfo.getItemInfos().get(0).getSkuInfos().get(0).getSkuId();//無sku也會返回到sku級,skuid為0activityCheckInfoMap.put(activitySeckillCheckInfo.getActivityId()+"_"+goodsLibId+"_"+goodsId+"_"+skuId,activitySeckillCheckInfo);}Map<String, ItemDetailsInfo> itemInfoMap = getGoodsInfos(seckillPostVo.getShopId(),seckillPostVo.getSeckillOrderInfoList());Integer expireMinute = seckillOrderCheckResponse.getResult().getExpireMinute();for(WmMarketingSeckillPostVo.SeckillOrderInfo seckillOrderInfo :seckillPostVo.getSeckillOrderInfoList()){ItemDetailsInfo itemInfo = itemInfoMap.get(seckillOrderInfo.getGoodsId()+"_"+seckillOrderInfo.getGoodsLibId());Long marketGuid = seckillOrderInfo.getMarketingGuid();Long goodsId = seckillOrderInfo.getGoodsId();Long goodsLibId = seckillOrderInfo.getGoodsLibId();String goodsSkuId = seckillOrderInfo.getGoodsSkuId()==null?"0":seckillOrderInfo.getGoodsSkuId();//無sku也會返回到sku級,skuid為0ActivitySeckillCheckInfo activitySeckillCheckInfo = activityCheckInfoMap.get(marketGuid+"_"+goodsLibId+"_"+goodsId+"_"+goodsSkuId);String activityName = activitySeckillCheckInfo.getActivityName();//秒殺價BigDecimal price = itemInfo.getPrice();for(ActivitySeckillCheckItemInfo activitySeckillCheckItemInfo : activitySeckillCheckInfo.getItemInfos()){if(activitySeckillCheckItemInfo.getGoodsId().longValue() == seckillOrderInfo.getGoodsId().longValue() && activitySeckillCheckItemInfo.getGoodsLibId().longValue() == seckillOrderInfo.getGoodsLibId().longValue()){for(ActivitySeckillCheckSkuInfo seckillCheckSkuInfo : activitySeckillCheckItemInfo.getSkuInfos()){if(seckillCheckSkuInfo.getSkuId().equals("0")||seckillCheckSkuInfo.getSkuId().equals(seckillOrderInfo.getGoodsSkuId())){price = seckillCheckSkuInfo.getPrice();}}}}JSONObject orderJson = createOrderDetail(marketGuid,activityName,expireMinute,seckillOrderInfo.getGoodsNum(), goodsId, goodsSkuId, price, itemInfo);seckillOrderList.add(orderJson);}return new CommonResult(seckillOrderList);}