通过redis实现的一个抢红包流程,仅做模拟【上】
建议结合下一篇一起看
数据结构+基础设施
数据结构
这里通过spring-data-jpa+mysql实现DB部分的处理,其中有lombok的参与
@MappedSuperclass @Data @NoArgsConstructor @AllArgsConstructor public class BaseEntity {//公共基础实体字段 @Id //标识主键 公用主键 @GeneratedValue //递增序列 private Long id; @Column(updatable = false) //不允许修改 @CreationTimestamp //创建时自动赋值 private Date createTime; @UpdateTimestamp //修改时自动修改 private Date updateTime; }
@Entity //标识这是个jpa数据库实体类 @Table @Data //lombok getter setter tostring @ToString(callSuper = true) //覆盖tostring 包含父类的字段 @Slf4j //SLF4J log @Builder //biulder模式 @NoArgsConstructor //无参构造函数 @AllArgsConstructor //全参构造函数 @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) public class RedPacketInfo extends BaseEntity implements Serializable {//红包信息表 private String red_packet_id;//红包ID private int total_amount;//总金额 private int total_packet;//总红包数 private int remaining_amount;//剩余金额 private int remaining_packet;//剩余红包数 private String user_id;//发红包用户ID }
@Entity //标识这是个jpa数据库实体类 @Table @Data //lombok getter setter tostring @ToString(callSuper = true) //覆盖tostring 包含父类的字段 @Slf4j //SLF4J log @Builder //biulder模式 @NoArgsConstructor //无参构造函数 @AllArgsConstructor //全参构造函数 @JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) public class RedPacketRecord extends BaseEntity implements Serializable {//抢红包记录表 private int amount; private String red_packet_id; private String user_id; }
REDIS数据结构
REDIS对于一个红包存储3部分信息:
1、KEY:红包ID+_TAL_PACKET VALUE:红包剩余数量
2、KEY:红包ID+_TOTAL_AMOUNT VALUE:红包剩余金额
3、KEY:红包ID+_lock VALUE:红包分布式锁
操作REDIS基础方法
private static final TimeUnit SECONDS = TimeUnit.SECONDS; private static final long DEFAULT_TOMEOUT = 5; private static final int SLEEPTIME = 50; /** * 获取分布式锁 2019 * @param lockKey * @param timeout * @param unit */ public boolean getLock(String lockKey, String value, long timeout, TimeUnit unit){ boolean lock = false; while (!lock) { //设置key自己的超时时间 lock = redisTemplate.opsForValue().setIfAbsent(lockKey, value,timeout,unit); if (lock) { // 已经获取了这个锁 直接返回已经获得锁的标识 return lock; } try { //暂停50ms,重新循环 Thread.sleep(SLEEPTIME); } catch (InterruptedException e) { e.printStackTrace(); } } return lock; } /** * 按照默认方式获得分布式锁 2019 * @param lockKey * @return */ public boolean getLock(String lockKey){ return getLock(lockKey,String.valueOf(new Date().getTime()),DEFAULT_TOMEOUT,SECONDS); }
/** * 获取指定 key 的值 * * @param key * @return */ public String get(String key) { return redisTemplate.opsForValue().get(key); } /** * 设置指定 key 的值 * * @param key * @param value */ public void set(String key, String value) { redisTemplate.opsForValue().set(key, value); }
DAO
public interface RedPacketInfoRepository extends JpaRepository<RedPacketInfo, Long> { @Query("select o from RedPacketInfo o where o.red_packet_id=:redPacketId") public RedPacketInfo findByRedPacketId(@Param("redPacketId") String redPacketId); }
public interface RedPacketRecordRepository extends JpaRepository<RedPacketRecord,Long> { }
配置
@Component @EnableAsync//开启异步注解,回写处 public class RedPacketConfig implements ApplicationRunner {
//启动自动发一个红包 @Autowired RedPacketService redPacketService; @Override public void run(ApplicationArguments args) throws Exception { String userId = "001"; redPacketService.handOut(userId,10000,20); } /** * 引入随机数组件 * @return */ @Bean public RandomValuePropertySource randomValuePropertySource(){ return new RandomValuePropertySource("RedPackeRandom"); } }
发红包
发红包通常没有特别需要处理高并发的点
/** * 发红包 * @param userId * @param total_amount 单位为分,不允许有小数点 * @param tal_packet * @return */ public RedPacketInfo handOut(String userId,int total_amount,int tal_packet){ RedPacketInfo redPacketInfo = new RedPacketInfo(); redPacketInfo.setRed_packet_id(genRedPacketId(userId)); redPacketInfo.setTotal_amount(total_amount); redPacketInfo.setTotal_packet(tal_packet); redPacketInfo.setRemaining_amount(total_amount); redPacketInfo.setRemaining_packet(tal_packet); redPacketInfo.setUser_id(userId); redPacketInfoRepository.save(redPacketInfo); redisUtil.set(redPacketInfo.getRed_packet_id()+TAL_PACKET, tal_packet+""); redisUtil.set(redPacketInfo.getRed_packet_id()+TOTAL_AMOUNT, total_amount+""); return redPacketInfo; }
/** * 组织红包ID * @return */ private String genRedPacketId(String userId){ String redpacketId = userId+"_"+new Date().getTime()+"_"+redisUtil.incrBy("redpacketid",1); return redpacketId; }
抢红包
详见代码注释
/** * 抢红包 * @param userId * @param redPacketId * @return */ public GrabResult grab(String userId, String redPacketId){ Date begin = new Date(); String msg = "红包已经被抢完!"; boolean resultFlag = false; double amountdb = 0.00; try{ //抢红包的过程必须保证原子性,此处加分布式锁 if(redisUtil.getLock(redPacketId+"_lock")) { RedPacketRecord redPacketRecord = new RedPacketRecord().builder().red_packet_id(redPacketId) .user_id(userId).build(); //如果没有红包了,则返回 if (Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET)) <= 0) { }else { //抢红包过程 //获取剩余金额 单位分 int remaining_amount = Integer.parseInt(redisUtil.get(redPacketId + TOTAL_AMOUNT)); //获取剩余红包数 int remaining_packet = Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET)); //计算本次抢红包金额 //计算公式:remaining_amount/remaining_packet*2 //如果只剩下一个红包,则余额全由这次的人获得 int amount = remaining_amount; if (remaining_packet != 1) { int maxAmount = remaining_amount / remaining_packet * 2; amount = Integer.parseInt(randomValuePropertySource.getProperty("random.int[0," + maxAmount + "]").toString()); } //与redis进行incrBy应该原子,并且2次与redis交互还有一定性能消耗,通过lua脚本实现更为妥当 redisUtil.incrBy(redPacketId + TAL_PACKET, -1); redisUtil.incrByFloat(redPacketId + TOTAL_AMOUNT, -amount); //准备返回结果 redPacketRecord.setAmount(amount); amountdb = amount / 100.00; msg = "恭喜你抢到红包,红包金额" + amountdb + "元!"; resultFlag = true; //异步记账 try { redPacketCallBackService.callback(userId, redPacketId, Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET)), Integer.parseInt(redisUtil.get(redPacketId + TOTAL_AMOUNT)), amount); } catch (Exception e) { log.error(e.getMessage(), e); } } } }finally { //解锁redis分布式锁 redisUtil.unLock(redPacketId+"_lock"); } Date end = new Date(); System.out.println(msg+",剩余红包:"+redisUtil.get(redPacketId + TAL_PACKET)+"个,本次抢红包消耗:"+(end.getTime()-begin.getTime())+"毫秒"); return new GrabResult().builder().msg(msg).resultFlag(resultFlag).amount(amountdb).red_packet_id(redPacketId).user_id(userId).build(); }
异步入账
/** * @program: redis * @description: 回写信息 * @author: X-Pacific zhang * @create: 2019-04-30 11:36 **/ @Service public class RedPacketCallBackService { @Autowired private RedPacketInfoRepository redPacketInfoRepository; @Autowired private RedPacketRecordRepository redPacketRecordRepository; /** * 回写红包信息表、抢红包表 */ @Async @Transactional(propagation = Propagation.REQUIRES_NEW) public void callback(String userId,String redPacketId,int remaining_packet,int remaining_amount,int amount) throws Exception { //校验 RedPacketInfo redPacketInfo = redPacketInfoRepository.findByRedPacketId(redPacketId); if(redPacketInfo.getRemaining_packet() <= 0 || redPacketInfo.getRemaining_amount() < amount){ throw new Exception("红包余额错误,本次抢红包失败!"); } //先更新红包信息表 redPacketInfo.setRemaining_packet(remaining_packet); redPacketInfo.setRemaining_amount(remaining_amount); redPacketInfoRepository.save(redPacketInfo); //新增抢红包信息 RedPacketRecord redPacketRecord = new RedPacketRecord().builder() .user_id(userId).red_packet_id(redPacketId).amount(amount).build(); redPacketRecordRepository.save(redPacketRecord); } }
测试抢红包
@Test public void testConcurrent(){ String redPacketId = "001_1556677154968_19"; // System.out.println(redPacketInfoRepository.findByRedPacketId("001_1556619425512_5")); Date begin = new Date(); for(int i = 0;i < 200;i++) { Thread thread = new Thread(() -> { String userId = "user_" + randomValuePropertySource.getProperty("random.int(10000)").toString(); redPacketService.grab(userId, redPacketId); }); thread.start(); } Date end = new Date(); System.out.println("合计消耗:"+(end.getTime() - begin.getTime())); try { Thread.sleep(100000); } catch (InterruptedException e) { e.printStackTrace(); } }