秒杀、支付等场景,用户频繁点击按钮,会造成同一时刻调用多次接口【第一次请求接口还没响应数据,用户又进行了第二次请求】,造成数据异常和网络拥堵。添加用户锁,在用户第二次点击按钮时,拦击用户请求。限制用户在操作未完成前不能再进行下一次相同操作
Dul
:用户锁注解,自定义锁注解,然后给需要加锁的方法加上此注解
DistributedUserLock
:锁接口
RedisDistributedUserLock
:分布式锁实现类
DistributedUserLockAspect
:切面类【核心】
自定义锁注解,利用切面给所有加注解的方法加分布式锁防止用户重复点击按钮。
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的key
lockKey += "_" + token;
try {
// 加锁成功,说明请求已经处理完,可以继续访问
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);
}
}
/**
* 分布式用户锁
* 限制用户在操作未完成前不能再进行下一次相同操作
*
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Dul {
/**
* 锁定的时间防止死锁,单位秒,默认10
*
* @return 同步锁定的时间
*/
int lockTimeSeconds() default 10;
/**
* 时间单位
*
* @return 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
默认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);
}
/**
* redis 分布式锁
*/
@SuppressWarnings({"UnusedReturnValue", "NullableProblems", "unused", "RedundantThrows"})
@Component
public class RedisDistributedUserLock implements DistributedUserLock {
/**
* 锁存在
*/
private final static Long TYPE_LOCK = 1L;
/**
* 缓存
*/
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void lock() {
throw new UnsupportedOperationException();
}
@Override
public void lockInterruptibly() throws InterruptedException {
throw new InterruptedException();
}
@Override
public boolean tryLock() {
throw new UnsupportedOperationException();
}
/**
* 锁
* @param lockKey 锁
* @param expireSeconds 锁有效时间
* @param timeUnit 时间单位
* @return true 可以进行操作,false 锁已存在
* @throws InterruptedException
*/
@Override
public 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);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
throw new UnsupportedOperationException();
}
/**
* 解锁
* @param lockKey 锁
*/
@Override
public void unlock(String lockKey) {
redisTemplate.delete(lockKey);
}
@Override
public void unlock() {
throw new UnsupportedOperationException();
}
@SuppressWarnings("NullableProblems")
@Override
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
/**
* 用户操作锁Aspect
*/
@Aspect
@Component
@Order(-1)
public class DistributedUserLockAspect {
/**
* 分布式锁
*/
@Autowired
private DistributedUserLock distributedUserLock;
@Autowired
private 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的key
lockKey += "_" + token;
try {
// 加锁成功,说明请求已经处理完,可以继续访问
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)
@Override
public 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("活动状态已变更,请重新下单");
}
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为0
activityCheckInfoMap.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为0
ActivitySeckillCheckInfo 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);
}