优化通过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[] -- 红包余量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
优化通过redis实现的一个抢红包流程【下】的更多相关文章
- 通过redis实现的一个抢红包流程,仅做模拟【上】
建议结合下一篇一起看 下一篇 数据结构+基础设施 数据结构 这里通过spring-data-jpa+mysql实现DB部分的处理,其中有lombok的参与 @MappedSuperclass @Dat ...
- 我把阿里、腾讯、字节跳动、美团等Android性能优化实战整合成了一个PDF文档
安卓开发大军浩浩荡荡,经过近十年的发展,Android技术优化日异月新,如今Android 11.0 已经发布,Android系统性能也已经非常流畅,可以在体验上完全媲美iOS. 但是,到了各大厂商手 ...
- ***Redis hash是一个string类型的field和value的映射表.它的添加、删除操作都是O(1)(平均)。hash特别适合用于存储对象
http://redis.readthedocs.org/en/latest/hash/hset.html HSET HSET key field value (存一个对象的时候key存) 将哈希 ...
- Django缓存优化之redis
Redis 概述 Redis 是一个开源的Inmemory key-value 存储系统,性能高,很大程度上补偿了 memcached 的不足.支持多种存储类型,包括 string, list, se ...
- Redis深入学习笔记(一)Redis启动数据加载流程
这两年使用Redis从单节点到主备,从主备到一主多从,再到现在使用集群,碰到很多坑,所以决定深入学习下Redis工作原理并予以记录. 本系列主要记录了Redis工作原理的一些要点,当然配置搭建和使用这 ...
- 使用Redis List简单实现抢红包
在这里不讨论抢红包的算法,只用redis简单尝试解决抢红包.借助redis单线程和List的POP方法. static void Main(string[] args) { IRedisHelper ...
- redis学习笔记——命令执行流程
基础知识部分 如果需要掌握Redis的整个命令的执行过程,那么必须掌握一些基本的概念!否则根本看不懂,下面我就一些在我看来必备的基础知识进行总结,希望能为后面命令的整个执行过程做铺垫. 事件 Redi ...
- Activity 学习(二) 搭建第一个Activity流程框架
本次示例使用的IDER测试完成 测试背景 : xx饿了去饭店吃饭 需要先和服务员点餐 点完餐后服务员将菜品传递给厨师制作 制作完成后吃饱 一 :创建流程图 创建上一篇测试成功出现的BpmnFil ...
- Controllers返回View的一个完整流程
详细说明一个MVC框架下,返回一个view的原理.如下图: 上图粗略的说明了一个返回View的流程,细节如下: 1.定义Model类: 2.定义接口添加接口约束为class: 3.定义接口实现类,即对 ...
随机推荐
- hdu 1848 Fibonacci again and again(SG函数)
Fibonacci again and again HDU - 1848 任何一个大学生对菲波那契数列(Fibonacci numbers)应该都不会陌生,它是这样定义的: F(1)=1; F(2)= ...
- asddf
https://docs.saltstack.com/en/getstarted/fundamentals/index.html https://pypi.org/simple/cherrypy/ 安 ...
- 爬虫(AJEX)——豆瓣动态页面
工具:python3 解释:Ajax 是一种用于创建快速动态网页的技术,在无需重新加载整个网页的情况下,能够更新部分网页的技术. 目标:爬取使用Ajex结束的豆瓣网页 import urllib.re ...
- mac终端快捷键
mac终端快捷键: http://www.jianshu.com/p/e6c364084c22
- UVALive - 6440
题目链接:https://vjudge.net/contest/241341#problem/G Indonesia, as well as some neighboring Southeast As ...
- UVALive - 6442
题目链接:https://vjudge.net/contest/241341#problem/I 题目大意:输入t,t组样例,输入n,m,有n个圆槽,m个硬币,接下来m行代表每个硬币所在的位子,要求你 ...
- kettle5.4ODBC和OCI连接配置
1.kettle 5.4 使用JDBC连接的时候报错(测试不同的数据库,发现只是连接11gRAC 的时候会报JDBC的错误) 具体报错如下 java.sql.SQLException: 建数据库连接出 ...
- Spark编程环境搭建及WordCount实例
基于Intellij IDEA搭建Spark开发环境搭建 基于Intellij IDEA搭建Spark开发环境搭——参考文档 ● 参考文档http://spark.apache.org/docs/la ...
- PyQt学习笔记
---------------个人学习笔记--------------- 1.QtWidgets.QApplication.instance().quit() 方法可退出当前窗体 2.self.Qla ...
- Asp.net中的ViewState用法
Session,ViewState用法基本理论:session值是保存在服务器内存上,那么,可以肯定,大量的使用session将导致服务器负担加重. 而viewstate由于只是将数据存入到页面隐藏控 ...