建議結合下一篇一起看
數據結構+基礎設施
數據結構
這里通過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(); } }