|
|
@ -20,13 +20,23 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
|
|
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.redisson.api.RLock; |
|
|
|
import org.redisson.api.RedissonClient; |
|
|
|
import org.springframework.beans.BeanUtils; |
|
|
|
import org.springframework.beans.factory.annotation.Autowired; |
|
|
|
import org.springframework.data.redis.core.StringRedisTemplate; |
|
|
|
import org.springframework.data.redis.core.script.DefaultRedisScript; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
import org.springframework.transaction.annotation.Propagation; |
|
|
|
import org.springframework.transaction.annotation.Transactional; |
|
|
|
|
|
|
|
import javax.annotation.Resource; |
|
|
|
import java.time.Duration; |
|
|
|
import java.util.Arrays; |
|
|
|
import java.util.Date; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
/** |
|
|
@ -41,9 +51,14 @@ import java.util.stream.Collectors; |
|
|
|
@Service |
|
|
|
public class CouponServiceImpl extends ServiceImpl<CouponMapper, CouponEntity> implements CouponService { |
|
|
|
|
|
|
|
@Resource |
|
|
|
private RedissonClient redissonClient; |
|
|
|
@Resource |
|
|
|
private CouponRecordMapper couponRecordMapper; |
|
|
|
|
|
|
|
@Autowired |
|
|
|
private StringRedisTemplate redisTemplate; |
|
|
|
|
|
|
|
@Override |
|
|
|
public Map<String, Object> pageCouponActivity(int page, int size) { |
|
|
|
Page<CouponEntity> pageInfo = new Page<>(page, size); |
|
|
@ -60,38 +75,109 @@ public class CouponServiceImpl extends ServiceImpl<CouponMapper, CouponEntity> i |
|
|
|
return pageMap; |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public JsonData addCoupon(Long couponId, CouponCategoryEnum category) { |
|
|
|
LoginUser loginUser = LoginInterceptor.threadLocal.get(); |
|
|
|
CouponEntity coupon = baseMapper.selectOne( |
|
|
|
Wrappers.<CouponEntity>lambdaQuery().eq(CouponEntity::getId, couponId) |
|
|
|
.eq(CouponEntity::getCategory, category.name())); |
|
|
|
//优惠券是否可以领取
|
|
|
|
this.checkCoupon(coupon, loginUser.getId()); |
|
|
|
//构建领劵记录
|
|
|
|
CouponRecordEntity couponRecord = new CouponRecordEntity(); |
|
|
|
BeanUtils.copyProperties(coupon, couponRecord); |
|
|
|
couponRecord.setCreateTime(new Date()); |
|
|
|
couponRecord.setUseState(CouponStateEnum.NEW.name()); |
|
|
|
couponRecord.setUserId(loginUser.getId()); |
|
|
|
couponRecord.setUserName(loginUser.getName()); |
|
|
|
couponRecord.setCouponId(couponId); |
|
|
|
couponRecord.setId(null); |
|
|
|
//扣减库存
|
|
|
|
// int rows = baseMapper.reduceStock(couponId);
|
|
|
|
//⾼并发下扣减劵库存,采⽤乐观锁,当前stock做版本号,延伸多种防⽌超卖的问题,⼀次只能领取1张
|
|
|
|
// 数据库添加字段: version INT DEFAULT 1, 根据当前的版本号更新
|
|
|
|
int rows = baseMapper.reduceStockOpt(couponId, coupon.getVersion()); |
|
|
|
if (rows == 1) { |
|
|
|
//库存扣减成功才保存
|
|
|
|
couponRecordMapper.insert(couponRecord); |
|
|
|
} else { |
|
|
|
log.warn("发放优惠券失败:{},⽤ 户:{}", coupon, loginUser); |
|
|
|
throw new BizException(BizCodeEnum.COUPON_NO_STOCK); |
|
|
|
|
|
|
|
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRED) |
|
|
|
@Override |
|
|
|
public JsonData addCoupon(Long couponId, CouponCategoryEnum category) { |
|
|
|
LoginUser loginUser = LoginInterceptor.threadLocal.get(); |
|
|
|
String lockKey = "lock:coupon:" + couponId; |
|
|
|
RLock rLock = redissonClient.getLock(lockKey); |
|
|
|
//多个线程进入,会阻塞等待释放锁
|
|
|
|
rLock.lock(); |
|
|
|
log.info("领劵接口加锁成功:{}", Thread.currentThread().getId()); |
|
|
|
try { |
|
|
|
CouponEntity coupon = baseMapper.selectOne( |
|
|
|
Wrappers.<CouponEntity>lambdaQuery() |
|
|
|
.eq(CouponEntity::getId, couponId).eq(CouponEntity::getCategory, category.name())); |
|
|
|
//优惠券是否可以领取
|
|
|
|
this.checkCoupon(coupon, loginUser.getId()); |
|
|
|
//构建领劵记录
|
|
|
|
CouponRecordEntity couponRecord = new CouponRecordEntity(); |
|
|
|
BeanUtils.copyProperties(coupon, couponRecord); |
|
|
|
couponRecord.setCreateTime(new Date()); |
|
|
|
couponRecord.setUseState(CouponStateEnum.NEW.name()); |
|
|
|
couponRecord.setUserId(loginUser.getId()); |
|
|
|
couponRecord.setUserName(loginUser.getName()); |
|
|
|
couponRecord.setCouponId(couponId); |
|
|
|
couponRecord.setId(null); |
|
|
|
//扣减库存
|
|
|
|
int rows = baseMapper.reduceStock(couponId); |
|
|
|
if (rows == 1) { |
|
|
|
//库存扣减成功才保存
|
|
|
|
couponRecordMapper.insert(couponRecord); |
|
|
|
} else { |
|
|
|
log.warn("发放优惠券失败:{},⽤户:{}", coupon, loginUser); |
|
|
|
throw new BizException(BizCodeEnum.COUPON_NO_STOCK); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
rLock.unlock(); |
|
|
|
log.info("解锁成功"); |
|
|
|
} |
|
|
|
return JsonData.buildSuccess(); |
|
|
|
} |
|
|
|
return JsonData.buildSuccess(); |
|
|
|
} |
|
|
|
|
|
|
|
public JsonData addCouponOld(Long couponId, CouponCategoryEnum category) { |
|
|
|
// synchronized (this) {
|
|
|
|
// String key = "coupon:"+couponId;
|
|
|
|
// setnx
|
|
|
|
// if(redisTemplate.opsForValue().setIfAbsent(key,"1")){
|
|
|
|
// redisTemplate.expire(key,30,TimeUnit.SECONDS);
|
|
|
|
// }
|
|
|
|
// setnx setex
|
|
|
|
// if(redisTemplate.opsForValue().setIfAbsent(key,"1",30,TimeUnit.SECONDS)){
|
|
|
|
// }else{
|
|
|
|
// }
|
|
|
|
LoginUser loginUser = LoginInterceptor.threadLocal.get(); |
|
|
|
String uuid = CommonUtil.generateUUID(); |
|
|
|
String lockKey = "lock:coupon:" + couponId; |
|
|
|
//避免锁过期,一般配久一点
|
|
|
|
Boolean lockFlag = redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, Duration.ofMinutes(10)); |
|
|
|
if (lockFlag) { |
|
|
|
log.info("加锁成功:{}", couponId); |
|
|
|
try { |
|
|
|
CouponEntity coupon = baseMapper.selectOne( |
|
|
|
Wrappers.<CouponEntity>lambdaQuery().eq(CouponEntity::getId, couponId) |
|
|
|
.eq(CouponEntity::getCategory, category.name())); |
|
|
|
//优惠券是否可以领取
|
|
|
|
this.checkCoupon(coupon, loginUser.getId()); |
|
|
|
//构建领劵记录
|
|
|
|
CouponRecordEntity couponRecord = new CouponRecordEntity(); |
|
|
|
BeanUtils.copyProperties(coupon, couponRecord); |
|
|
|
couponRecord.setCreateTime(new Date()); |
|
|
|
couponRecord.setUseState(CouponStateEnum.NEW.name()); |
|
|
|
couponRecord.setUserId(loginUser.getId()); |
|
|
|
couponRecord.setUserName(loginUser.getName()); |
|
|
|
couponRecord.setCouponId(couponId); |
|
|
|
couponRecord.setId(null); |
|
|
|
//扣减库存
|
|
|
|
int rows = baseMapper.reduceStock(couponId); |
|
|
|
//⾼并发下扣减劵库存,采⽤乐观锁,当前stock做版本号,延伸多种防⽌超卖的问题,⼀次只能领取1张
|
|
|
|
// 数据库添加字段: version INT DEFAULT 1, 根据当前的版本号更新
|
|
|
|
// int rows = baseMapper.reduceStockOpt(couponId, coupon.getVersion());
|
|
|
|
if (rows == 1) { |
|
|
|
//库存扣减成功才保存
|
|
|
|
couponRecordMapper.insert(couponRecord); |
|
|
|
} else { |
|
|
|
log.warn("发放优惠券失败:{},⽤ 户:{}", coupon, loginUser); |
|
|
|
throw new BizException(BizCodeEnum.COUPON_NO_STOCK); |
|
|
|
} |
|
|
|
} finally { |
|
|
|
//解锁
|
|
|
|
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; |
|
|
|
Integer result = redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(lockKey), uuid); |
|
|
|
log.info("解锁:{}", result); |
|
|
|
} |
|
|
|
} else { |
|
|
|
//加锁失败
|
|
|
|
try { |
|
|
|
TimeUnit.SECONDS.sleep(1); |
|
|
|
} catch (InterruptedException e) { |
|
|
|
log.error("自旋失败"); |
|
|
|
} |
|
|
|
addCoupon(couponId, category); |
|
|
|
} |
|
|
|
return JsonData.buildSuccess(); |
|
|
|
} |
|
|
|
|
|
|
|
private CouponVO beanProcess(CouponEntity entity) { |
|
|
|
CouponVO couponVO = new CouponVO(); |
|
|
|