|
|
@@ -71,6 +71,7 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
|
|
|
private static final String REDPACKET_REMAININGLOTS_KEY = "live:red:remainingLots:";
|
|
|
private static final String REDPACKET_REMAININGNUM_KEY = "live:red:remainingNum:";
|
|
|
private static final String REDPACKET_CLAIM_KEY = "live:red:claim:";
|
|
|
+ private static final String REDPACKET_CONF_CACHE_KEY = "live:red:conf:"; // 红包配置缓存
|
|
|
|
|
|
@Autowired
|
|
|
private LiveRedConfMapper baseMapper;
|
|
|
@@ -130,10 +131,17 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
|
|
|
double score = localDateTime.atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli();
|
|
|
redisCache.redisTemplate.opsForZSet().add(cacheKey, String.valueOf(liveRedConf.getRedId()), score);
|
|
|
redisCache.redisTemplate.expire(cacheKey, 30, TimeUnit.MINUTES);
|
|
|
+
|
|
|
+ // 将红包配置缓存到 Redis(用于高并发查询)
|
|
|
+ String redConfCacheKey = REDPACKET_CONF_CACHE_KEY + liveRedConf.getRedId();
|
|
|
+ redisCache.setCacheObject(redConfCacheKey, JSONUtil.toJsonStr(liveRedConf), liveRedConf.getDuration().intValue() + 5, TimeUnit.MINUTES);
|
|
|
+ log.info("红包配置已缓存到 Redis,redId: {}, liveId: {}", liveRedConf.getRedId(), liveRedConf.getLiveId());
|
|
|
} else {
|
|
|
// 其他
|
|
|
redisCache.deleteObject(REDPACKET_REMAININGLOTS_KEY + liveRedConf.getRedId());
|
|
|
redisCache.deleteObject(cacheKey);
|
|
|
+ // 删除红包配置缓存
|
|
|
+ redisCache.deleteObject(REDPACKET_CONF_CACHE_KEY + liveRedConf.getRedId());
|
|
|
redStatusUpdate(CollUtil.newHashSet(liveRedConf.getRedId()));
|
|
|
}
|
|
|
return baseMapper.updateLiveRedConf(liveRedConf);
|
|
|
@@ -226,137 +234,151 @@ public class LiveRedConfServiceImpl implements ILiveRedConfService {
|
|
|
baseMapper.deleteById(redId);
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 用户领取红包
|
|
|
- */
|
|
|
+
|
|
|
@Override
|
|
|
- @Transactional
|
|
|
public R claimRedPacket(RedPO red) {
|
|
|
- // String claimKey = REDPACKET_CLAIM_KEY + red.getRedId();
|
|
|
- Object o = redisCache.hashGet(String.format(LiveKeysConstant. LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId()), String.valueOf(red.getUserId()));
|
|
|
- if (ObjectUtil.isNotEmpty(o)) {
|
|
|
- return R.error("您已经领取过红包了!");
|
|
|
- }
|
|
|
- /*try {*/
|
|
|
-/* //获取红包锁w
|
|
|
- if (!tryLock(claimKey, red.getUserId().toString(), 5)) {
|
|
|
- return R.error("您已经领取过红包了!");
|
|
|
- }*/
|
|
|
-
|
|
|
- LiveRedConf conf = baseMapper.selectLiveRedConfByRedId(red.getRedId());
|
|
|
- if (conf == null || conf.getRedStatus() != 1) {
|
|
|
- return R.error("手慢了,红包已结束~");
|
|
|
- }
|
|
|
- //redis剩余红包数
|
|
|
- // 平均分 暂时不适用redis 记录红包数
|
|
|
- Long integral = calculateIntegralAverage(conf);
|
|
|
- if (0L == integral) {
|
|
|
- return R.error("手慢了,红包被抢完了~");
|
|
|
- }
|
|
|
-
|
|
|
- // 更新数据库
|
|
|
-/*
|
|
|
- Date now = new Date();
|
|
|
- conf.setTotalSend(conf.getTotalSend() + 1);
|
|
|
- conf.setRemaining(Math.toIntExact(conf.getTotalLots() - conf.getTotalSend()));
|
|
|
- conf.setUpdateTime(now);
|
|
|
- baseMapper.updateLiveRedConf(conf);
|
|
|
-*/
|
|
|
-
|
|
|
- // 最后更新缓存
|
|
|
- if (getRemaining(red.getRedId()) <= 0 || !decreaseRemainingLotsIfPossible(red.getRedId())) {
|
|
|
- LiveRedConf liveRedConf = new LiveRedConf();
|
|
|
- liveRedConf.setRedId(red.getRedId());
|
|
|
- liveRedConf.setRedStatus(2L);
|
|
|
- baseMapper.updateLiveRedConf(liveRedConf);
|
|
|
- Set<String> range = CollUtil.newHashSet(String.valueOf(red.getRedId()));
|
|
|
-// finishRedStatusBySetIds(range);
|
|
|
- updateDbByRed(liveRedConf);
|
|
|
- return R.error("手慢了,红包已被抢完~");
|
|
|
- }
|
|
|
- // 记录用户红包
|
|
|
+// * 1. 使用 Redis HSETNX 原子操作保证幂等性(每个用户只能领取一次)
|
|
|
+// * 2. 从 Redis 读取红包配置(提高响应速度)
|
|
|
+// * 3. 使用 Redis 原子操作减少剩余数量
|
|
|
+// * 4. 异步更新数据库
|
|
|
+ String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId());
|
|
|
+ String userIdStr = String.valueOf(red.getUserId());
|
|
|
+
|
|
|
+ // 1. 使用 Redis HSETNX 原子操作保证幂等性(每个用户只能领取一次)
|
|
|
+ // 先尝试在 Redis 中标记用户已领取(原子操作,保证高并发安全)
|
|
|
LiveUserRedRecord record = new LiveUserRedRecord();
|
|
|
record.setRedId(red.getRedId());
|
|
|
record.setLiveId(red.getLiveId());
|
|
|
record.setUserId(red.getUserId());
|
|
|
- record.setIntegral(integral);
|
|
|
record.setCreateTime(new Date());
|
|
|
|
|
|
- // 双重检查:先检查 Redis(已有),再检查数据库(防止重复领取)
|
|
|
- String redisKey = String.format(LiveKeysConstant.LIVE_HOME_PAGE_CONFIG_RED, red.getLiveId(), red.getRedId());
|
|
|
- Object redisRecord = redisCache.hashGet(redisKey, String.valueOf(red.getUserId()));
|
|
|
- if (ObjectUtil.isNotEmpty(redisRecord)) {
|
|
|
- log.warn("用户 {} 在 Redis 中已存在红包记录 redId: {},跳过重复处理", red.getUserId(), red.getRedId());
|
|
|
- return R.error("您已经领取过红包了!");
|
|
|
- }
|
|
|
-
|
|
|
- LiveUserRedRecord queryRecord = new LiveUserRedRecord();
|
|
|
- queryRecord.setUserId(red.getUserId());
|
|
|
- queryRecord.setRedId(red.getRedId());
|
|
|
- List<LiveUserRedRecord> existingRecords = userRedRecordMapper.selectLiveUserRedRecordList(queryRecord);
|
|
|
- if (existingRecords != null && !existingRecords.isEmpty()) {
|
|
|
- log.warn("用户 {} 在数据库中已存在红包记录 redId: {},跳过重复处理", red.getUserId(), red.getRedId());
|
|
|
- // 如果数据库已有记录但 Redis 没有,同步到 Redis
|
|
|
- redisCache.hashPut(redisKey, String.valueOf(red.getUserId()), JSONUtil.toJsonStr(existingRecords.get(0)));
|
|
|
+ // 使用 HSETNX 原子操作:如果字段不存在则设置,返回 true;如果已存在则返回 false
|
|
|
+ Boolean claimed = redisCache.hashPutIfAbsent(redisKey, userIdStr, "claimed");
|
|
|
+ if (Boolean.FALSE.equals(claimed)) {
|
|
|
+ // 用户已经领取过(Redis 中已存在记录)
|
|
|
+ log.debug("用户 {} 已领取过红包 redId: {}(Redis 检查)", red.getUserId(), red.getRedId());
|
|
|
return R.error("您已经领取过红包了!");
|
|
|
+ } else {
|
|
|
+ redisCache.expire(redisKey, 24, TimeUnit.HOURS);
|
|
|
}
|
|
|
|
|
|
- // 先插入数据库记录(使用数据库约束防止重复)
|
|
|
- int insertResult = userRedRecordMapper.insertLiveUserRedRecord(record);
|
|
|
- if (insertResult <= 0) {
|
|
|
- log.error("插入红包记录失败,userId: {}, redId: {}", red.getUserId(), red.getRedId());
|
|
|
- return R.error("领取红包失败,请稍后重试");
|
|
|
- }
|
|
|
-
|
|
|
- // 插入后再次验证,防止并发插入导致重复
|
|
|
- List<LiveUserRedRecord> verifyRecords = userRedRecordMapper.selectLiveUserRedRecordList(queryRecord);
|
|
|
- if (verifyRecords != null && verifyRecords.size() > 1) {
|
|
|
- // 发现重复记录,删除刚插入的记录并回滚
|
|
|
- log.error("检测到重复红包记录,userId: {}, redId: {},记录数: {}", red.getUserId(), red.getRedId(), verifyRecords.size());
|
|
|
- // 删除最后插入的记录(通常是当前请求插入的)
|
|
|
- userRedRecordMapper.deleteLiveUserRedRecordById(record.getId());
|
|
|
- return R.error("您已经领取过红包了!");
|
|
|
+ try {
|
|
|
+ // 2. 从 Redis 读取红包配置(优先从缓存读取,提高响应速度)
|
|
|
+ String redConfCacheKey = REDPACKET_CONF_CACHE_KEY + red.getRedId();
|
|
|
+ Object confCache = redisCache.getCacheObject(redConfCacheKey);
|
|
|
+ LiveRedConf conf = null;
|
|
|
+
|
|
|
+ if (confCache != null) {
|
|
|
+ try {
|
|
|
+ conf = JSONUtil.toBean(confCache.toString(), LiveRedConf.class);
|
|
|
+ log.debug("从 Redis 缓存读取红包配置,redId: {}", red.getRedId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("从 Redis 缓存解析红包配置失败,从数据库读取,redId: {}", red.getRedId(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果 Redis 中没有配置,从数据库读取并缓存
|
|
|
+ if (conf == null) {
|
|
|
+ conf = baseMapper.selectLiveRedConfByRedId(red.getRedId());
|
|
|
+ if (conf != null && conf.getRedStatus() == 1) {
|
|
|
+ // 缓存到 Redis
|
|
|
+ redisCache.setCacheObject(redConfCacheKey, JSONUtil.toJsonStr(conf), conf.getDuration().intValue() + 5, TimeUnit.MINUTES);
|
|
|
+ log.debug("从数据库读取红包配置并缓存到 Redis,redId: {}", red.getRedId());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证红包状态
|
|
|
+ if (conf == null || conf.getRedStatus() != 1) {
|
|
|
+ // 回滚:删除 Redis 中的标记
|
|
|
+ redisCache.hashDelete(redisKey, userIdStr);
|
|
|
+ return R.error("手慢了,红包已结束~");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 使用 Redis 原子操作减少剩余数量
|
|
|
+ if (getRemaining(red.getRedId()) <= 0 || !decreaseRemainingLotsIfPossible(red.getRedId())) {
|
|
|
+ // 回滚:删除 Redis 中的标记
|
|
|
+ redisCache.hashDelete(redisKey, userIdStr);
|
|
|
+ // 更新红包状态为已结束
|
|
|
+ LiveRedConf liveRedConf = new LiveRedConf();
|
|
|
+ liveRedConf.setRedId(red.getRedId());
|
|
|
+ liveRedConf.setRedStatus(2L);
|
|
|
+ baseMapper.updateLiveRedConf(liveRedConf);
|
|
|
+ // 删除配置缓存
|
|
|
+ redisCache.deleteObject(redConfCacheKey);
|
|
|
+ return R.error("手慢了,红包已被抢完~");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算积分(平均分)
|
|
|
+ Long integral = calculateIntegralAverage(conf);
|
|
|
+ if (0L == integral) {
|
|
|
+ // 回滚:删除 Redis 中的标记
|
|
|
+ redisCache.hashDelete(redisKey, userIdStr);
|
|
|
+ return R.error("手慢了,红包被抢完了~");
|
|
|
+ }
|
|
|
+
|
|
|
+ record.setIntegral(integral);
|
|
|
+
|
|
|
+ // 4. 更新用户积分(同步操作,保证数据一致性)
|
|
|
+ BigDecimal balanceAmount = BigDecimal.valueOf(integral);
|
|
|
+ int updateResult = fsUserScrmMapper.incrIntegral(red.getUserId(), balanceAmount);
|
|
|
+ if (updateResult <= 0) {
|
|
|
+ // 回滚:删除 Redis 中的标记和恢复剩余数量
|
|
|
+ redisCache.hashDelete(redisKey, userIdStr);
|
|
|
+ // 恢复剩余数量
|
|
|
+ String remainingKey = REDPACKET_REMAININGLOTS_KEY + red.getRedId();
|
|
|
+ redisCache.redisTemplate.opsForValue().increment(remainingKey);
|
|
|
+ log.error("更新用户余额失败,userId: {}, balance: {}", red.getUserId(), balanceAmount);
|
|
|
+ return R.error("更新用户余额失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 更新 Redis 缓存中的记录(包含完整信息)
|
|
|
+ record.setCreateTime(new Date());
|
|
|
+ redisCache.hashPut(redisKey, userIdStr, JSONUtil.toJsonStr(record));
|
|
|
+
|
|
|
+ // 6. 异步更新数据库(提高响应速度,不阻塞用户)
|
|
|
+ // 查询用户当前余额(用于积分日志)
|
|
|
+ com.fs.hisStore.domain.FsUserScrm user = fsUserScrmMapper.selectFsUserById(red.getUserId());
|
|
|
+ Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
|
|
|
+
|
|
|
+
|
|
|
+ final LiveUserRedRecord finalRecord = record;
|
|
|
+ final LiveRedConf finalConf = conf;
|
|
|
+ final Long finalIntegral = integral;
|
|
|
+ final Long finalCurrentIntegral = currentIntegral;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 插入红包记录
|
|
|
+ userRedRecordMapper.insertLiveUserRedRecord(finalRecord);
|
|
|
+
|
|
|
+ // 添加积分变动记录
|
|
|
+ FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
|
|
|
+ integralLogs.setUserId(red.getUserId());
|
|
|
+ integralLogs.setIntegral(finalIntegral);
|
|
|
+ integralLogs.setBalance(finalCurrentIntegral);
|
|
|
+ integralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_26.getValue());
|
|
|
+ integralLogs.setBusinessId(String.valueOf(red.getRedId()));
|
|
|
+ integralLogs.setBusinessType(Math.toIntExact(finalConf.getRedId()));
|
|
|
+ integralLogs.setStatus(0);
|
|
|
+ integralLogs.setCreateTime(new Date());
|
|
|
+ fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
|
|
|
+
|
|
|
+ log.debug("异步更新数据库成功,userId: {}, redId: {}, integral: {}", red.getUserId(), red.getRedId(), finalIntegral);
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 数据库更新失败不影响用户领取(已更新积分),记录日志即可
|
|
|
+ log.error("异步更新数据库失败,userId: {}, redId: {}, integral: {}", red.getUserId(), red.getRedId(), finalIntegral, e);
|
|
|
}
|
|
|
|
|
|
+
|
|
|
|
|
|
- // 查询用户当前余额
|
|
|
- com.fs.hisStore.domain.FsUserScrm user = fsUserScrmMapper.selectFsUserById(red.getUserId());
|
|
|
- Long currentIntegral = user.getIntegral() != null ? user.getIntegral() : 0L;
|
|
|
- Long newIntegral = currentIntegral + integral;
|
|
|
-
|
|
|
- // 添加余额变动记录
|
|
|
- FsUserIntegralLogs integralLogs = new FsUserIntegralLogs();
|
|
|
- integralLogs.setUserId(red.getUserId());
|
|
|
- integralLogs.setIntegral(integral);
|
|
|
- integralLogs.setBalance(newIntegral);
|
|
|
- integralLogs.setLogType(FsUserIntegralLogTypeEnum.TYPE_26.getValue()); // 3表示分享获得积分,可根据实际情况调整
|
|
|
- integralLogs.setBusinessId(String.valueOf(red.getRedId()));
|
|
|
- integralLogs.setBusinessType(Math.toIntExact(conf.getRedId())); // 1表示直播红包
|
|
|
- integralLogs.setStatus(0);
|
|
|
- integralLogs.setCreateTime(new Date());
|
|
|
- fsUserIntegralLogsMapper.insertFsUserIntegralLogs(integralLogs);
|
|
|
- // 更新用户余额
|
|
|
- BigDecimal balanceAmount = BigDecimal.valueOf(integral);
|
|
|
- int updateResult = fsUserScrmMapper.incrIntegral(red.getUserId(), balanceAmount);
|
|
|
- if (updateResult <= 0) {
|
|
|
- log.error("更新用户余额失败,userId: {}, balance: {}", red.getUserId(), balanceAmount);
|
|
|
- // 回滚:删除已插入的记录
|
|
|
- userRedRecordMapper.deleteLiveUserRedRecordById(record.getId());
|
|
|
- return R.error("更新用户余额失败");
|
|
|
+
|
|
|
+ return R.ok("恭喜您成功抢到" + integral + "芳华币");
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 发生异常,回滚 Redis 标记
|
|
|
+ redisCache.hashDelete(redisKey, userIdStr);
|
|
|
+ log.error("领取红包异常,userId: {}, redId: {}", red.getUserId(), red.getRedId(), e);
|
|
|
+ return R.error("领取红包失败,请稍后重试");
|
|
|
}
|
|
|
-
|
|
|
- // 最后更新 Redis 缓存(确保原子性:先插入数据库,再更新 Redis)
|
|
|
- redisCache.hashPut(redisKey, String.valueOf(red.getUserId()), JSONUtil.toJsonStr(record));
|
|
|
-
|
|
|
- // WebSocket 通知
|
|
|
- //String msg = String.format("用户 %d 抢到了红包 %d,获得 %d 芳华币", userId, redId, integral);
|
|
|
- //WebSocketServer.notifyUsers(msg);
|
|
|
- return R.ok("恭喜您成功抢到" + integral + "芳华币");
|
|
|
-/* } catch (Exception e) {
|
|
|
- e.printStackTrace();
|
|
|
- log.error("抢红包异常:" + e.getMessage());
|
|
|
- }*/
|
|
|
- // return R.error("抢红包异常");
|
|
|
}
|
|
|
|
|
|
@Override
|