沉默的背影 X-Pacific

keep learning

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

 

 

 

posted @ 2019-05-01 10:42  乂墨EMO  阅读(1718)  评论(0编辑  收藏  举报