优化通过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.定义接口实现类,即对 ...
随机推荐
- 洛谷P2534 [AHOI2012]铁盘整理
P2534 [AHOI2012]铁盘整理 题目描述 输入输出格式 输入格式: 共两行.第一行为铁盘个数N(1<=N<=50),第二行为N个不同的正整数,分别为从上到下的铁盘的半径R.(1& ...
- 批处理打开和关闭oracle11g 服务
也许我们经常会有这样一些困惑,如果让oracle随开机启动,我们得电脑内存会被占用殆尽,运行速度会变的异常慢,但是,手动一个一个去启动和关闭,又会非常麻烦.为了解决这个问题,我们有一个办法,那就是写两 ...
- IE浏览器不支持Promise对象
1. 安装babel-polyfill插件转换 npm install --save-dev babel-polyfill 2. 在webpack中引入babel-polyfill 在webpack. ...
- 洛谷 P1547 Out of Hay (最小生成树)
嗯... 题目链接:https://www.luogu.org/problemnew/show/P1547 思路: 嗯...既然题中已经说了是最小生成树,那么是需要在最小生成树的模板上稍作修改即可.要 ...
- 自增长 auto_increment
auto_increment :自动编号,一般与主键组合使用.一个表里面只有一个自增默认情况下,起始值为1,每次的增量为1. 例子:create table tb5( id int primar ...
- 74th LeetCode Weekly Contest Valid Number of Matching Subsequences
Given string S and a dictionary of words words, find the number of words[i] that is a subsequence of ...
- 1089 Insert or Merge(25 分)
According to Wikipedia: Insertion sort iterates, consuming one input element each repetition, and gr ...
- linux查看硬盘空间,删除大文件
df -Phdu -h --max-depth=1du -sh /u02/weblogic/user_projects/domains/logsdu -sh /u02/mysqlfind / -siz ...
- (转)Linux系统基础网络配置老鸟精华篇
Linux系统基础网络配置老鸟精华篇 原文:http://blog.51cto.com/oldboy/784625 对于linux高手看似简单的网络配置问题,也许要说出所以然来也并不轻松,因此仍然有太 ...
- (转)Linux修改eth2到eth0(70-persistent-net.rules)
之前在公司提供的虚拟机器上面,一直有个问题用着很不舒服,为什么它的IP选择的设备的eth2的,但是我在/etc/sysconfig/network-scrpts/下面也没有找到ifcfg-eth2的配 ...