通過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();
        }
    }

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM