优化通过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.定义接口实现类,即对 ...
随机推荐
- 魔卡少女(cardcaptor)——线段树
题目 [题目描述] 君君是中山大学的四年级学生.有一天在家不小心开启了放置在爸爸书房中的一本古书.于是,君君把放在书中最上面的一张牌拿出来观摩了一下,突然掀起一阵大风把书中的其她所有牌吹散到各地.这时 ...
- bzoj2055: 80人环游世界(可行流)
传送门 表示完全看不懂最小费用可行流…… 据某大佬说 我们考虑拆点,然后进行如下连边 $s$向$a_i$连边,权值$0$,容量$[0,m]$ $a_i$向$a_i'$连边,权值$0$容量$[v_i,v ...
- 深入解析Android Design包——Behavior
已经说过了,在AndroidDesign包中主要有两个核心概念:一是NestedScroll,另一个就是Behavior. 相比于NestedScroll这个概念来说,Behavior分析起来会难很多 ...
- Python中list的复制及深拷贝与浅拷贝探究
在Python中,经常要对一个list进行复制.对于复制,自然的就有深拷贝与浅拷贝问题.深拷贝与浅拷贝的区别在于,当从原本的list复制出新的list之后,修改其中的任意一个是否会对另一个造成影响,即 ...
- thinkphp5 join使用注意
A表有id,name,time等字段, B表有id,type,uid,email,address等字段. A表中的id和B表中的uid对应. Db::table(A表)->alias('a') ...
- Apache服务器配置虚拟域名
我在别处发的帖子 http://www.52pojie.cn/thread-599829-1-1.html
- mysql /etc/my.cnf
[client] port=3306 socket = /tmp/mysql.sock default-character-set=utf8 [mysqld] # 1 general config p ...
- myeclipse 上安装 Maven
myeclipse 上安装 Maven3 环境准备: JDK 1.6 Maven 3.0.4 myeclipse 8.6.1 安装 Maven 之前要求先确定你的 JDK 已经安装配置完成.Mav ...
- 新手常见Python运行时错误
经过整理与在实际中遇到的问题,将新手经常遇到的汇总下,以便自己犯傻又这么干了 1)"SyntaxError :invalid syntax",语法错误 A.查看是否在 if , e ...
- 程序代码里出现illegal character '\ufeff' 和 expected class or object definition的解决办法(图文详解)
不多说,直接上干货! 问题详情 问题分析 可能原因导致1:你的程序也许,是在他人那里复制而来,会导致这样的问题. 可能原因导致2:由于页面编码造成的. 可能原因导致1的解决办法 这个,好比,我 ...