diff --git a/nla-common/pom.xml b/nla-common/pom.xml index 6d549e0..c65b71f 100644 --- a/nla-common/pom.xml +++ b/nla-common/pom.xml @@ -72,6 +72,12 @@ redis.clients jedis + + + org.redisson + redisson + 3.10.1 + io.jsonwebtoken diff --git a/nla-common/src/main/java/cn/nla/common/config/RedissonConfig.java b/nla-common/src/main/java/cn/nla/common/config/RedissonConfig.java new file mode 100644 index 0000000..d66a23d --- /dev/null +++ b/nla-common/src/main/java/cn/nla/common/config/RedissonConfig.java @@ -0,0 +1,35 @@ +package cn.nla.common.config; + +import lombok.Data; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Redisson配置 + */ +@Configuration +@Data +public class RedissonConfig { + @Value("${spring.redis.host}") + private String redisHost; + @Value("${spring.redis.port}") + private String redisPort; + @Value("${spring.redis.password}") + private String redisPwd; + /** + * 配置分布式锁的redisson + */ + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + //单机方式 + config.useSingleServer().setPassword(redisPwd).setAddress("redis://" + redisHost + ":" + redisPort); + //集群 + // config.useClusterServers().addNodeAddress("redis://127.0.0.1:6379","redis://127.0.0.2:6379") + return Redisson.create(config); + } +} diff --git a/nla-common/src/main/java/cn/nla/common/util/CommonUtil.java b/nla-common/src/main/java/cn/nla/common/util/CommonUtil.java index 34f2482..a337fbe 100644 --- a/nla-common/src/main/java/cn/nla/common/util/CommonUtil.java +++ b/nla-common/src/main/java/cn/nla/common/util/CommonUtil.java @@ -11,6 +11,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.security.MessageDigest; import java.util.Random; +import java.util.UUID; /** * 工具类 @@ -119,6 +120,14 @@ public class CommonUtil { return saltString.toString(); } + + /** + * 生成uuid + */ + public static String generateUUID() { + return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32); + } + /** * 响应json数据给前端 */ diff --git a/nla-coupon-service/src/main/java/cn/nla/coupon/service/impl/CouponServiceImpl.java b/nla-coupon-service/src/main/java/cn/nla/coupon/service/impl/CouponServiceImpl.java index d42afbf..7f9a178 100644 --- a/nla-coupon-service/src/main/java/cn/nla/coupon/service/impl/CouponServiceImpl.java +++ b/nla-coupon-service/src/main/java/cn/nla/coupon/service/impl/CouponServiceImpl.java @@ -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 implements CouponService { + @Resource + private RedissonClient redissonClient; @Resource private CouponRecordMapper couponRecordMapper; + @Autowired + private StringRedisTemplate redisTemplate; + @Override public Map pageCouponActivity(int page, int size) { Page pageInfo = new Page<>(page, size); @@ -60,38 +75,109 @@ public class CouponServiceImpl extends ServiceImpl i return pageMap; } -@Override -public JsonData addCoupon(Long couponId, CouponCategoryEnum category) { - LoginUser loginUser = LoginInterceptor.threadLocal.get(); - CouponEntity coupon = baseMapper.selectOne( - Wrappers.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.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.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(); diff --git a/nla-coupon-service/src/main/resources/application.yml b/nla-coupon-service/src/main/resources/application.yml index 6b0c61e..72f25af 100644 --- a/nla-coupon-service/src/main/resources/application.yml +++ b/nla-coupon-service/src/main/resources/application.yml @@ -3,6 +3,11 @@ server: spring: application: name: nla-coupon-service + redis: + host: 127.0.0.1 + port: 6379 + database: 0 + password: yuan123456 #数据库配置 datasource: driver-class-name: com.mysql.cj.jdbc.Driver