上一篇文章通过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[] -- 红包余量ID
local packet_amount_id = KEYS[] -- 红包余额ID
local user_id = KEYS[] -- 用户ID 用于校验是否已经抢过红包
local red_packet_id = KEYS[] -- 红包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) > then -- 如果有红包才做真正的抢红包动作
local flag = redis.call('BF.EXISTS', bloom_name, user_id) -- 通过布隆过滤器校验是否存在
if(flag == ) then -- 如果存在(可能存在)这是个待优化点
return "" -- 不能完全确定用户已经存在
elseif(tonumber(rcount) ~= ) then -- 不存在则计算抢红包金额,并实施真正的扣减
local maxamount = ramount / rcount * ;
amount = math.random(,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 ""
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

优化通过redis实现的一个抢红包流程【下】的更多相关文章

  1. 通过redis实现的一个抢红包流程,仅做模拟【上】

    建议结合下一篇一起看 下一篇 数据结构+基础设施 数据结构 这里通过spring-data-jpa+mysql实现DB部分的处理,其中有lombok的参与 @MappedSuperclass @Dat ...

  2. 我把阿里、腾讯、字节跳动、美团等Android性能优化实战整合成了一个PDF文档

    安卓开发大军浩浩荡荡,经过近十年的发展,Android技术优化日异月新,如今Android 11.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS. 但是,到了各大厂商手 ...

  3. ***Redis hash是一个string类型的field和value的映射表.它的添加、删除操作都是O(1)(平均)。hash特别适合用于存储对象

    http://redis.readthedocs.org/en/latest/hash/hset.html HSET HSET key field value   (存一个对象的时候key存) 将哈希 ...

  4. Django缓存优化之redis

    Redis 概述 Redis 是一个开源的Inmemory key-value 存储系统,性能高,很大程度上补偿了 memcached 的不足.支持多种存储类型,包括 string, list, se ...

  5. Redis深入学习笔记(一)Redis启动数据加载流程

    这两年使用Redis从单节点到主备,从主备到一主多从,再到现在使用集群,碰到很多坑,所以决定深入学习下Redis工作原理并予以记录. 本系列主要记录了Redis工作原理的一些要点,当然配置搭建和使用这 ...

  6. 使用Redis List简单实现抢红包

    在这里不讨论抢红包的算法,只用redis简单尝试解决抢红包.借助redis单线程和List的POP方法. static void Main(string[] args) { IRedisHelper ...

  7. redis学习笔记——命令执行流程

    基础知识部分 如果需要掌握Redis的整个命令的执行过程,那么必须掌握一些基本的概念!否则根本看不懂,下面我就一些在我看来必备的基础知识进行总结,希望能为后面命令的整个执行过程做铺垫. 事件 Redi ...

  8. Activity 学习(二) 搭建第一个Activity流程框架

    本次示例使用的IDER测试完成 测试背景 : xx饿了去饭店吃饭  需要先和服务员点餐  点完餐后服务员将菜品传递给厨师制作  制作完成后吃饱 一 :创建流程图 创建上一篇测试成功出现的BpmnFil ...

  9. Controllers返回View的一个完整流程

    详细说明一个MVC框架下,返回一个view的原理.如下图: 上图粗略的说明了一个返回View的流程,细节如下: 1.定义Model类: 2.定义接口添加接口约束为class: 3.定义接口实现类,即对 ...

随机推荐

  1. Docker 快速安装&搭建 Ngnix 环境,并配置反向代理

    欢迎关注个人微信公众号: 小哈学Java, 文末分享阿里 P8 高级架构师吐血总结的 <Java 核心知识整理&面试.pdf>资源链接!! 个人网站: https://www.ex ...

  2. print和sys.stdout

    print print语句执行的操作是一个写操作,把我们从外设输入的数据写到了stdout流,并进行了一些特定的格式化.和文件方法不同,在执行打印操作是,不需要将对象转换为字符串(print已经帮我们 ...

  3. Servlet和HTTP请求协议

    Servlet和HTTP请求协议 Servlet和HTTP请求协议 Servlet和HTTP请求协议 有待补充... servlet servlet applet 概念 servlet是运行在服务器上 ...

  4. C 语言实例 - 计算一个数的 n 次方

    C 语言实例 - 计算一个数的 n 次方 计算一个数的 n 次方,例如: ,其中 为基数, 为指数. 实例 - 使用 while #include <stdio.h> int main() ...

  5. CF447A DZY Loves Hash 模拟

    DZY has a hash table with p buckets, numbered from 0 to p - 1. He wants to insert n numbers, in the ...

  6. python 3.7 生成数据库文档

    开发阶段数据库总是有变动,开发人员需要维护文档给相关人员使用,故编写一个脚本自动生成数据库文档 生成的excel如下 import cx_Oracle import os from openpyxl ...

  7. 零散的JS和node.js小知识

    JS的连续赋值和曾经出现的怪异情况 let a=1; let b=a=3; 如上的真实赋值过程 => a=1 => a=3 => b=3 => 一般来说,等号是从右向左赋值的 ...

  8. PHP 获取acm近期比赛

    <?php // author: Moore Jiang. ini_set('display_errors',1); //错误信息 ini_set('display_startup_errors ...

  9. js通过行列获取表格中input的值

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <HTML> <HEAD ...

  10. Tree--lecture08

    1.二叉树 完全二叉树(complete binary tree):除了最下面一层都是满的,最下面一层也是优先排列在左边.这样的话父亲节点和孩子节点就在序号上面有关系: 父亲节点为n,那么子节点的编号 ...