OpenRedPacketWithRedissonLockService.java 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. package com.webchat.ugc.service.redpacket;
  2. import com.webchat.common.constants.RedPacketConstants;
  3. import com.webchat.common.enums.RedisKeyEnum;
  4. import com.webchat.domain.vo.response.RedPacketBaseVO;
  5. import org.redisson.api.RLock;
  6. import org.redisson.api.RedissonClient;
  7. import org.springframework.beans.factory.annotation.Autowired;
  8. import org.springframework.stereotype.Service;
  9. import org.springframework.util.Assert;
  10. import java.math.BigDecimal;
  11. import java.util.concurrent.ThreadLocalRandom;
  12. /**
  13. * 方案一:基于redisson分布式锁实现
  14. *
  15. * 注意:
  16. * 这里红包剩余金额以及剩余可拆分份额,完全依赖redis(最好缓存失效可以查询保证一致性)
  17. */
  18. @Service
  19. public class OpenRedPacketWithRedissonLockService extends AbstractOpenRedPacketService {
  20. @Autowired
  21. private RedissonClient redissonClient;
  22. /**
  23. * 采用实时计算红包金额方式
  24. *
  25. * @param redPacket
  26. * @param userId
  27. * @return
  28. */
  29. @Override
  30. public int openRedPacket(RedPacketBaseVO redPacket, String userId) {
  31. String lockKey = RedisKeyEnum.LOCK_OPEN_RED_PACKET.getKey(String.valueOf(redPacket.getId()));
  32. RLock lock = redissonClient.getLock(lockKey);
  33. try {
  34. // 阻塞等待获取锁
  35. lock.lock();
  36. return doOpenRedPacket(redPacket, userId);
  37. } catch (Exception e) {
  38. throw e;
  39. } finally {
  40. if (lock.isLocked() && lock.isHeldByCurrentThread()) {
  41. lock.unlock();
  42. }
  43. }
  44. }
  45. /**
  46. * 红包拆分
  47. *
  48. * @param redPacket
  49. * @param userId
  50. * @return
  51. */
  52. private int doOpenRedPacket(RedPacketBaseVO redPacket, String userId) {
  53. Long redPacketId = redPacket.getId();
  54. /**
  55. * 1. 读取当前红包剩余金额
  56. *
  57. * 10、10、10
  58. */
  59. int balanceAmount = this.getBalance(redPacketId);
  60. Assert.isTrue(balanceAmount > 0, "红包被瓜分完了");
  61. /**
  62. * 2. 获取红包当前剩余可拆分份额
  63. * 2、2、2
  64. */
  65. int gradCount = this.getGradCount(redPacketId);
  66. Assert.isTrue(gradCount > 0, "来晚了");
  67. /**
  68. * 3. 判断红包类型(算法)
  69. * 3.1 普通红包:非最后一个拆分,平均瓜分(总金额 / 总人数);最后一个拆分取剩余金额
  70. * 3.2 拼手气:”二倍均值算法“ 为当前用户随机红包金额
  71. *
  72. * 二倍均值红包算法:
  73. * 示例:100元红包、份10份
  74. * 1️⃣ 100元 ===> 10000分,保证所有金额为整数(因为我们小数点精确到后两位)
  75. * 2️⃣ 第1份红包:
  76. * 均值AVG = 余额 / 剩余份额
  77. * 2倍均值= 2 * 均值AVG
  78. * 用于随机生成红包金额范围:[0.01元/1分钱 ~ 2 * 均值AVG] --> randomAmount(1050分)
  79. * 剩余:9份,余额 10000 - 1050 = 8950分
  80. * 3️⃣ 第2份红包:
  81. * 均值AVG = 8950分 / 9
  82. * randomAmount(1566分)
  83. * 剩余:8份,余额 8950分 - 1566分
  84. *
  85. * ……
  86. * 最后一次:
  87. * randomAmount = 10000分 - 历史总拆分金额
  88. * 剩余:0份,余额 0
  89. *
  90. */
  91. // 单位为分
  92. int openAmount = 0;
  93. if (gradCount == 1) {
  94. // 最后一个可拆分名额
  95. openAmount = balanceAmount;
  96. } else {
  97. // 二倍均值
  98. BigDecimal avgAmount = new BigDecimal(balanceAmount).divide(new BigDecimal(gradCount), 0, BigDecimal.ROUND_HALF_UP);
  99. BigDecimal avgAmount2 = avgAmount.multiply(new BigDecimal(2));
  100. int maxVal = avgAmount2.intValue();
  101. // 在[minVal, maxVal]之间随机一个红包金额:openAmount
  102. openAmount = ThreadLocalRandom.current().nextInt(RedPacketConstants.MIN_AMOUNT, maxVal + 1);
  103. }
  104. /**
  105. * 4. 写刷新红包剩余金额
  106. */
  107. balanceAmount = balanceAmount - openAmount;
  108. // 刷新当前红包剩余可拆分金额
  109. this.setBalance(redPacketId, balanceAmount);
  110. // 红包剩余可拆分次数原子-1
  111. this.increxGradCountCache(redPacketId, -1);
  112. // 将当前用户加入红包拆分用户名单缓存
  113. super.addUser2OpenRedPacketUsersCache(redPacketId, userId);
  114. if (balanceAmount == 0) {
  115. // 红包被拆分完
  116. super.updateRedPacketStatus(redPacketId, RedPacketConstants.RedPacketStatus.END);
  117. }
  118. return openAmount;
  119. }
  120. /**
  121. * 这里余额为啥是整数?
  122. * 因为我们会讲所用 单位元 转化为 分,同时保证元的小数点精确到后两位。
  123. *
  124. * @return 单位:分,1块钱,这里返回100分
  125. */
  126. private Integer getBalance(Long redPacketId) {
  127. String balanceKey = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
  128. String balanceCache = redisService.hget(balanceKey, String.valueOf(redPacketId));
  129. return Integer.valueOf(balanceCache);
  130. }
  131. /**
  132. * 刷新红包剩余可拆分金额
  133. *
  134. * @param redPacketId
  135. * @param balance 单位需要转为分
  136. */
  137. private void setBalance(Long redPacketId, Integer balance) {
  138. String balanceKey = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
  139. redisService.hset(balanceKey, String.valueOf(redPacketId), String.valueOf(balance),
  140. RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getExpireTime());
  141. }
  142. /**
  143. * 当前已瓜分数量
  144. *
  145. * @param redPacketId
  146. * @return
  147. */
  148. private Integer getGradCount(Long redPacketId) {
  149. String balanceKey = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
  150. String countCache = redisService.hget(balanceKey, String.valueOf(redPacketId));
  151. return Integer.valueOf(countCache);
  152. }
  153. /**
  154. *
  155. *
  156. *
  157. * @param redPacketId
  158. * @return
  159. */
  160. private void increxGradCountCache(Long redPacketId, long count) {
  161. String balanceKey = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
  162. redisService.hIncrementVal(balanceKey, String.valueOf(redPacketId), count);
  163. }
  164. /**
  165. * 初始化红包剩余可拆分金额
  166. *
  167. * @param redPacketId
  168. * @param balance 单位需要转为分
  169. */
  170. public void initBalanceCache(Long redPacketId, Integer balance) {
  171. String balanceKey = RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getKey();
  172. redisService.hset(balanceKey, String.valueOf(redPacketId), String.valueOf(balance),
  173. RedisKeyEnum.RED_PACKET_BALANCE_COUNT.getExpireTime());
  174. }
  175. /**
  176. * 初始化红包剩余可以瓜分数量
  177. *
  178. * @param redPacketId
  179. * @return
  180. */
  181. public void initGradCountCache(Long redPacketId, int gradCount) {
  182. String balanceKey = RedisKeyEnum.RED_PACKET_GRAD_COUNT.getKey();
  183. redisService.hset(balanceKey, String.valueOf(redPacketId), gradCount + "",
  184. RedisKeyEnum.RED_PACKET_GRAD_COUNT.getExpireTime());
  185. }
  186. }