优化通过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.定义接口实现类,即对 ...
随机推荐
- bzoj3681: Arietta(网络流)
传送门 主席树优化建图? 然而我连代码都看不懂 贴个题解好了->这里 //minamoto #include<iostream> #include<cstdio> #in ...
- nodejs + webpack4 + babel6 结合写Chrome浏览器插件记录
最近任务不忙,有时间了看一下Chrome插件相关的东西,于是想用nodejs + webpack写一个能直出插件的小工具. 1.nodejs + babel6 + webpack4 在写之前,是有把它 ...
- ssh-ssh整合(Struts2+Sprinig+hibernate)
在之前呢我们已经讲解过ssm以及ssm2的整合开发,今天我们进行ssh的整合,在之前已经有一篇整合ssh的文章,那是基于注解开发的,今天讲解的为基于配置文件注入方式进行开发.思路:spring管理hi ...
- 利用docker部署redis集群
目录 一.首先配置redis.conf文件,... 1 1.获取配置文件... 1 2.修改各配置文件的参数... 2 二.下载redis镜像.启动容器... 2 1.创建网络... 2 2.拉取镜像 ...
- Redis内核原理及读写一致企业级架构深入剖析1-综合组件环境实战
1 Redis 工作模型 redis实际上是个单线程工作模型,其拥有较多的数据结构,并支持丰富的数据操作,redis目前是原生支持cluster模式.如果需要缓存能够支持更复杂的结构和操作,基于以上原 ...
- AT2689 Prime Flip
传送门 这个题是真的巧妙 首先一个很巧妙的思路,差分 考虑假如\(a_i!=a_{i-1}\),则\(b_i=1\),否则\(b_i=0\) 这样一来,一个区间的翻转就变成了对于两个数的取反了 然后我 ...
- thinkphp5.1使用支付宝接口(沙箱环境)
接口文件以及沙箱的测试账号可以去支付宝开发中心获取,下面给出一个简单地例子 我新建了一个控制器Pay用来专门做支付 <?phpnamespace app\index\controller; us ...
- Shape Number (最小表示法)
题目链接 一个字符串,这个字符串的首尾是连在一起的,要求寻找一个位置,以该位置为起点的字符串的字典序在所有的字符串中中最小. #include <bits/stdc++.h> using ...
- vue中点击空白处隐藏弹框(用指令优雅地实现)
在写vue的项目的时候,弹框经常性出现,并要求点击弹框外面,关闭弹框,那么如何实现呢?且听我一一...不了,能实现效果就好 <template> <div> <div c ...
- Codeforces Round #365 (Div. 2) A
Description Mishka is a little polar bear. As known, little bears loves spending their free time pla ...