优化通过redis实现的一个抢红包流程【下】

上一篇文章通过redis实现的抢红包通过测试发现有严重的阻塞的问题,抢到红包的用户很快就能得到反馈,不能抢到红包的用户很久(10秒以上)都无法获得抢红包结果,起主要原因是:

1、用了分布式锁,导致所有的操作只能顺序排队,而后面没有抢到红包的需要等待前面抢红包的同学完事后他才能去看自己是否已经抢到红包

2、多次与redis交互,消耗了很多时间(交互一次大概是几十到上百毫秒),分布式锁本身也需要和redis交互

所以通过仔细打磨,我决定通过lua表达式来达到缩减redis交互次数以及保证高并发情况下与redis多个交互命令的原子性

优化1、优化抢红包流程

除了添加lua脚本来处理真正抢红包的过程,去掉了分布式锁,还在lua脚本中通过布隆过滤器校验用户是否抢过红包

      //抢红包的过程必须保证原子性,此处加分布式锁
        //但是用分布式锁,阻塞时间太久,导致部分线程需要阻塞10s以上,性能非常不好
        //如果没有红包了,则返回
        if (Integer.parseInt(redisUtil.get(redPacketId + TAL_PACKET)) > 0) {//有红包,才能有机会去真正的抢
            //真正抢红包的过程,通过lua脚本处理保证原子性,并减少与redis交互的次数
            // lua脚本逻辑中包含了计算抢红包金额
            //任何余额等瞬时信息都从这里快照取出,否则不准
            //如果我们在这里分开写逻辑,不保证原子性的情况下有可能造成前面获取的金额后面用的时候红包已经不是原来获取金额时的情况了,并且多次与redis交互耗时严重
            String result = grubFromRedis(redPacketId + TAL_PACKET, redPacketId + TOTAL_AMOUNT, userId, redPacketId);
            //准备返回结果

其中很多操作都压缩到了lua脚本中

local packet_count_id = KEYS[1] -- 红包余量ID
local packet_amount_id = KEYS[2] -- 红包余额ID
local user_id = KEYS[3] -- 用户ID 用于校验是否已经抢过红包
local red_packet_id = KEYS[4] -- 红包ID用于校验是否已经抢过红包
-- grub
local bloom_name = red_packet_id .. '_BLOOM_GRAB_REDPACKET'; -- 布隆过滤器ID
local rcount = redis.call('GET', packet_count_id) --  获取红包余量
local ramount = redis.call('GET', packet_amount_id) --  获取红包余额
local amount = ramount; --  默认红包金额为余额,用于只剩一个红包的情况
if  tonumber(rcount) > 0 then -- 如果有红包才做真正的抢红包动作
    local flag = redis.call('BF.EXISTS', bloom_name, user_id) -- 通过布隆过滤器校验是否存在
    if(flag == 1) then -- 如果存在(可能存在)这是个待优化点
        return "1" -- 不能完全确定用户已经存在
    elseif(tonumber(rcount) ~= 1) then -- 不存在则计算抢红包金额,并实施真正的扣减
        local maxamount = ramount / rcount * 2;
        amount = math.random(1,maxamount);
    end
    local result_2 = redis.call('DECR', packet_count_id)
    local result_3 = redis.call('DECRBY', packet_amount_id, amount)
    redis.call('BF.ADD', bloom_name, user_id)
    return amount .. "SPLIT" .. rcount
else
    return "0"
end

优化2、优化回写逻辑(用MQ替代更可靠、合适)

  @Async
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void callback(String userId,String redPacketId,int amount) throws Exception {
        log.info("用户:{},抢到当前红包:{},金额:{},回写成功!", userId, redPacketId, amount);
        //新增抢红包信息
        //不能用自增ID,已经调整
        RedPacketRecord redPacketRecord = new RedPacketRecord().builder()
                .user_id(userId).red_packet_id(redPacketId).amount(amount).build();
        redPacketRecord.setId(UUID.randomUUID().toString());
        redPacketRecordRepository.save(redPacketRecord);
  }

中间发现高并发情况下JPA+mysql自增ID有严重的死锁问题

所以调整了两个表的主键生成逻辑:

@MappedSuperclass
@Data
@NoArgsConstructor
@AllArgsConstructor
public class BaseEntity {
    @Id //标识主键 公用主键
//    @GeneratedValue //递增序列
    private String 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;
    private int total_amount;
    private int total_packet;
    private String user_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;
}
  @Transactional
    public RedPacketInfo handOut(String userId, int total_amount, int tal_packet) {
        RedPacketInfo redPacketInfo = new RedPacketInfo();
        redPacketInfo.setRed_packet_id(genRedPacketId(userId));
        redPacketInfo.setId(redPacketInfo.getRed_packet_id());
        redPacketInfo.setTotal_amount(total_amount);
        redPacketInfo.setTotal_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;
    }

测试代码

测试1000并发,抢10元20个红包,平均每人抢红包时间1秒之内(平均600ms),大大优于之前版本的抢红包数据

@GetMapping("/concurrent")
    public String concurrent(){
        RedPacketInfo redPacketInfo = redPacketService.handOut("zxp",1000,20);
        String redPacketId = redPacketInfo.getRed_packet_id();
        for(int i = 0;i < 1000;i++) {
            Thread thread = new Thread(() -> {
                String userId = "user_" + randomValuePropertySource.getProperty("random.int(10000)").toString();
                Date begin = new Date();
                GrabResult grabResult = redPacketService.grab(userId, redPacketId);
                Date end = new Date();
                log.info(grabResult.getMsg()+",本次消耗:"+(end.getTime()-begin.getTime()));
            });
            thread.start();
        }
        return "ok";
    }

Fork From GitHub

 fork from github

 

 

转载于:https://www.cnblogs.com/zxporz/p/10813709.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 确定数据结构 为了实现抢红包功能,我们需要用到Redis的有序集合(sorted set)数据结构。 假设我们有一个红包池,里面有100个红包,每个红包金额不同。我们可以把每个红包放到有序集合中,键为红包的编号,值为红包金额。 2. 生成红包编号和金额 在生成红包编号和金额时,我们可以使用随机数生成器。假设红包编号为1~100,红包金额为1~100元之间的随机数。生成红包编号和金额后,将它们放到有序集合中。 3. 抢红包 当用户抢红包时,我们需要从有序集合中随机取出一个红包,并将它的编号和金额返回给用户。同时,我们需要将这个红包从有序集合中删除,以确保每个红包只能被抢一次。 4. 实现代码 以下是一个简单的Python代码实现: ```python import redis import random # 连接Redis r = redis.Redis(host='localhost', port=6379, db=0) # 生成红包编号和金额 for i in range(1, 101): red_packet_id = str(i) red_packet_amount = random.randint(1, 100) r.zadd('red_packet_pool', {red_packet_id: red_packet_amount}) # 抢红包 def grab_red_packet(user_id): red_packet = r.zpopmin('red_packet_pool') if red_packet: red_packet_id, red_packet_amount = red_packet[0].decode(), red_packet[1] return {'user_id': user_id, 'red_packet_id': red_packet_id, 'red_packet_amount': red_packet_amount} else: return None ``` 在上面的代码中,我们使用了Redis的zadd()函数将红包编号和金额放到有序集合中,使用了zpopmin()函数随机取出一个红包并将其从有序集合中删除。grab_red_packet()函数用于实现抢红包功能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值