Переглянути джерело

领取优惠券库存加入分布式锁

zc 2 місяців тому
батько
коміт
95fe9c0755

+ 75 - 0
alien-store/src/main/java/shop/alien/store/config/BaseRedisService.java

@@ -12,6 +12,7 @@ import org.springframework.stereotype.Component;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
+import java.util.UUID;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -307,5 +308,79 @@ public class BaseRedisService {
         return stringRedisTemplate.opsForZSet().remove(type, (Object[]) members);
     }
 
+    /**
+     * 获取分布式锁
+     * @param lockKey 锁的标识
+     * @return 锁的value值,用于释放锁时验证,null表示获取失败
+     */
+    public String lock(String lockKey) {
+        return lock(lockKey, 30000, 10000);
+    }
+
+    /**
+     * 获取分布式锁
+     * @param lockKey 锁的标识
+     * @param expireTime 锁的过期时间(毫秒)
+     * @param acquireTimeout 获取锁的超时时间(毫秒)
+     * @return 锁的value值,用于释放锁时验证,null表示获取失败
+     */
+    public String lock(String lockKey, long expireTime, long acquireTimeout) {
+        // 生成唯一标识,用于释放锁时验证
+        String identifier = UUID.randomUUID().toString();
+        // 完整的锁键
+        String lockKeyWithPrefix = "inventory:lock:" + lockKey;
+        // 超时时间戳
+        long endTime = System.currentTimeMillis() + acquireTimeout;
+
+        while (System.currentTimeMillis() < endTime) {
+            // 尝试获取锁:SET NX PX
+            Boolean success = stringRedisTemplate.opsForValue()
+                    .setIfAbsent(lockKeyWithPrefix, identifier, expireTime, TimeUnit.MILLISECONDS);
+
+            if (Boolean.TRUE.equals(success)) {
+                // 获取锁成功
+                return identifier;
+            }
+
+            try {
+                // 短暂休眠,避免频繁尝试
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return null;
+            }
+        }
+
+        // 获取锁超时
+        return null;
+    }
+
+    /**
+     * 释放分布式锁
+     * @param lockKey 锁的标识
+     * @param identifier 锁的value值,用于验证
+     * @return 是否释放成功
+     */
+    public boolean unlock(String lockKey, String identifier) {
+        if (identifier == null) {
+            return false;
+        }
+
+        String lockKeyWithPrefix = "inventory:lock:" + lockKey;
+
+        // 使用Lua脚本确保释放锁的原子性
+        String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                "return redis.call('del', KEYS[1]) " +
+                "else " +
+                "return 0 " +
+                "end";
+
+        RedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);
+        Long result = stringRedisTemplate.execute(script,
+                Collections.singletonList(lockKeyWithPrefix),
+                identifier);
+
+        return result != null && result > 0;
+    }
 
 }

+ 26 - 1
alien-store/src/main/java/shop/alien/store/service/impl/LifeDiscountCouponUserServiceImpl.java

@@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.BeansException;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeCollect;
 import shop.alien.entity.store.LifeDiscountCoupon;
@@ -18,6 +19,7 @@ import shop.alien.mapper.LifeCollectMapper;
 import shop.alien.mapper.LifeDiscountCouponMapper;
 import shop.alien.mapper.LifeDiscountCouponUserMapper;
 import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.LifeDiscountCouponUserService;
 import shop.alien.util.common.constant.DiscountCouponEnum;
 
@@ -46,9 +48,20 @@ public class LifeDiscountCouponUserServiceImpl extends ServiceImpl<LifeDiscountC
 
     private final LifeDiscountCouponUserMapper lifeDiscountCouponUserMapper;
 
+    private final BaseRedisService baseRedisService;
+
+
     @Override
+    @Transactional(rollbackFor = Exception.class)
     public R<Boolean> receiveCoupon(LifeDiscountCouponUserDto lifeDiscountCouponUserDto) {
+        // 锁value
+        String identifier = null;
         try {
+            identifier = baseRedisService.lock(lifeDiscountCouponUserDto.getCouponId().toString());
+            if (StringUtils.isEmpty(identifier)) {
+                throw new RuntimeException("抢购人数过多,请稍后重试");
+            }
+
             Integer receiveQuantity = lifeDiscountCouponUserDto.getReceiveQuantity();
             LifeDiscountCoupon lifeDiscountCoupon = lifeDiscountCouponMapper.selectById(lifeDiscountCouponUserDto.getCouponId());
             // 判断是否在领取时间内
@@ -64,6 +77,15 @@ public class LifeDiscountCouponUserServiceImpl extends ServiceImpl<LifeDiscountC
             if (getStatus == 0) {
                 return R.fail("该优惠劵已经关闭领取");
             }
+
+            if(lifeDiscountCoupon.getSingleQty() != null && lifeDiscountCoupon.getSingleQty() <= 0){
+                return R.fail("已发放完毕");
+            }
+
+            if(lifeDiscountCoupon.getSingleQty() != null && lifeDiscountCoupon.getSingleQty() < receiveQuantity){
+                return R.fail("已发放完毕");
+            }
+
             String specifiedDay = lifeDiscountCoupon.getSpecifiedDay();
 
             //判断领取数量
@@ -107,9 +129,12 @@ public class LifeDiscountCouponUserServiceImpl extends ServiceImpl<LifeDiscountC
                     storeInfoMapper.update(null, new LambdaUpdateWrapper<StoreInfo>().eq(StoreInfo::getId, lifeCollect.getStoreId()).set(StoreInfo::getCollectNum, lifeCollectMapper.selectCount(queryWrapper)));
                 }
             }
-        } catch (BeansException e) {
+        } catch (Exception e) {
             log.error("LifeDiscountCouponController.receiveCoupon ERROR Msg=" + e.getMessage());
+            baseRedisService.unlock(lifeDiscountCouponUserDto.getCouponId().toString(), identifier);
             return R.fail("领取失败");
+        } finally {
+            baseRedisService.unlock(lifeDiscountCouponUserDto.getCouponId().toString(), identifier);
         }
         return R.success("领取成功");
     }