异步领券
把优惠券相关数据缓存到Redis中,基于Redis完成资格校验
最终我们要做的有:
- 生成兑换码时,将优惠券及对应兑换码序列号的最大值缓存到Redis中
- 改造兑换优惠券的功能,利用Redis完成资格校验,然后发送MQ消息(消息体中要增加传递兑换码的序列号)
- 改造领取优惠券的MQ监听器,添加标记兑换码状态为已兑换的功能
弊端:
- MQ挂掉
- 后面的用户券更新 失败,但是前面 已经记录用户领取成功了
redisTemplate.opsForHash().increment(key, userId.toString(), 1);
最终返回的是递增操作完成后该字段的新值。例如,若该字段原本存储的值为 5,执行此操作后,该字段的值会更新为 6,并且该方法会返回 6。
第一步,改造领取逻辑,实现基于Redis的领取资格校验。校验完成后不是立刻领取,而是发送MQ消息:
package com.tianji.promotion.service.impl;
// ...略
import static com.tianji.promotion.constants.PromotionConstants.COUPON_CODE_MAP_KEY;
import static com.tianji.promotion.constants.PromotionConstants.COUPON_RANGE_KEY;
/**
* <p>
* 用户领取优惠券的记录,是真正使用的优惠券信息 服务实现类
* </p>
*/
@Service
@RequiredArgsConstructor
public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCoupon> implements IUserCouponService {
private final CouponMapper couponMapper;
private final IExchangeCodeService codeService;
private final StringRedisTemplate redisTemplate;
private final RabbitMqHelper mqHelper;
@Override
@Lock(name = "lock:coupon:#{couponId}")
public void receiveCoupon(Long couponId) {
// 1.查询优惠券
Coupon coupon = queryCouponByCache(couponId);
if (coupon == null) {
throw new BadRequestException("优惠券不存在");
}
// 2.校验发放时间
LocalDateTime now = LocalDateTime.now();
if (now.isBefore(coupon.getIssueBeginTime()) || now.isAfter(coupon.getIssueEndTime())) {
throw new BadRequestException("优惠券发放已经结束或尚未开始");
}
// 3.校验库存
if (coupon.getIssueNum() >= coupon.getTotalNum()) {
throw new BadRequestException("优惠券库存不足");
}
Long userId = UserContext.getUser();
// 4.校验每人限领数量
// 4.1.查询领取数量
String key = PromotionConstants.USER_COUPON_CACHE_KEY_PREFIX + couponId;
Long count = redisTemplate.opsForHash().increment(key, userId.toString(), 1);
// 4.2.校验限领数量
if(count > coupon.getUserLimit()){
throw new BadRequestException("超出领取数量");
}
// 5.扣减优惠券库存
redisTemplate.opsForHash().increment(
PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId, "totalNum", -1);
// 6.发送MQ消息
UserCouponDTO uc = new UserCouponDTO();
uc.setUserId(userId);
uc.setCouponId(couponId);
mqHelper.send(MqConstants.Exchange.PROMOTION_EXCHANGE, MqConstants.Key.COUPON_RECEIVE, uc);
}
private Coupon queryCouponByCache(Long couponId) {
// 1.准备KEY
String key = PromotionConstants.COUPON_CACHE_KEY_PREFIX + couponId;
// 2.查询
Map<Object, Object> objMap = redisTemplate.opsForHash().entries(key);
if (objMap.isEmpty()) {
return null;
}
// 3.数据反序列化
return BeanUtils.mapToBean(objMap, Coupon.class, false, CopyOptions.create());
}
// ...略
}
第二步:监听MQ并领券
编写一个MQ监听器,监听领券的消息:
package com.tianji.promotion.handler;
import com.tianji.promotion.domain.dto.UserCouponDTO;
import