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