当前位置:网站首页>红包雨: Redis 和 Lua 的奇妙邂逅
红包雨: Redis 和 Lua 的奇妙邂逅
2022-06-27 10:14:00 【InfoQ】

1 整体流程

- 运营系统配置红包雨活动总金额以及红包个数,提前计算出各个红包的金额并存储到 Redis 中;
- 抢红包雨界面,用户点击屏幕上落下的红包,发起抢红包请求;
- TCP 网关接收抢红包请求后,调用答题系统抢红包 dubbo 服务,抢红包服务本质上就是执行 Lua 脚本,将结果通过 TCP 网关返回给前端;
- 用户若抢到红包,异步任务会从 Redis 中 获取抢得的红包信息,调用余额系统,将金额返回到用户账户。
2 红包 Redis 设计
- 同一活动,用户只能抢红包一次 ;
- 红包数量有限,一个红包只能被一个用户抢到。
- 运营预分配红包列表 ;

{
//红包编号
redPacketId : '365628617880842241'
//红包金额
amount : '12.21'
}
- 用户红包领取记录列表;

{
//红包编号
redPacketId : '365628617880842241'
//红包金额
amount : '12.21',
//用户编号
userId : '265628617882842248'
}
- 用户红包防重 Hash 表;

- 通过 hexist 命令判断红包领取记录防重 Hash 表中用户是否领取过红包 ,若用户未领取过红包,流程继续;
- 从运营预分配红包列表 rpop 出一条红包数据 ;
- 操作红包领取记录防重 Hash 表 ,调用 HSET 命令存储用户领取记录;
- 将红包领取信息 lpush 进入用户红包领取记录列表。
- 执行多个命令,是否可以保证原子性 , 若一个命令执行失败,是否可以回滚;
- 在执行过程中,高并发场景下,是否可以保持隔离性;
- 后面的步骤依赖前面步骤的结果。
3 事务原理
- 事务开启,使用 MULTI , 该命令标志着执行该命令的客户端从非事务状态切换至事务状态 ;
- 命令入队,MULTI 开启事务之后,客户端的命令并不会被立即执行,而是放入一个事务队列 ;
- 执行事务或者丢弃。如果收到 EXEC 的命令,事务队列里的命令将会被执行 ,如果是 DISCARD 则事务被丢弃。
redis> MULTI
OK
redis> SET msg "hello world"
QUEUED
redis> GET msg
QUEUED
redis> EXEC
1) OK
1) hello world


4 事务的ACID
4.1 原子性
redis> MULTI
OK
redis> SET msg "other msg"
QUEUED
redis> wrongcommand ### 故意写错误的命令
(error) ERR unknown command 'wrongcommand'
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
redis> GET msg
"hello world"
redis> MULTI
OK
redis> SET msg "other msg"
QUEUED
redis> SET mystring "I am a string"
QUEUED
redis> HMSET mystring name "test"
QUEUED
redis> SET msg "after"
QUEUED
redis> EXEC
1) OK
2) OK
3) (error) WRONGTYPE Operation against a key holding the wrong kind of value
4) OK
redis> GET msg
"after"
- 命令入队时报错, 会放弃事务执行,保证原子性;
- 命令入队时正常,执行 EXEC 命令后报错,不保证原子性;
4.2 隔离性
- 未提交读(read uncommitted)
- 提交读(read committed)
- 可重复读(repeatable read)
- 串行化(serializable)
- EXEC 命令执行前
- EXEC 命令执行后
4.3 持久性
- 没有配置 RDB 或者 AOF ,事务的持久性无法保证;
- 使用了 RDB模式,在一个事务执行后,下一次的 RDB 快照还未执行前,如果发生了实例宕机,事务的持久性同样无法保证;
- 使用了 AOF 模式;AOF 模式的三种配置选项 no 、everysec 都会存在数据丢失的情况 。always 可以保证事务的持久性,但因为性能太差,在生产环境一般不推荐使用。
4.4 一致性
- 维基百科
- 执行 EXEC 命令前,客户端发送的操作命令错误,事务终止,数据保持一致性;
- 执行 EXEC 命令后,命令和操作的数据类型不匹配,错误的命令会报错,但事务不会因为错误的命令而终止,而是会继续执行。正确的命令正常执行,错误的命令报错,从这个角度来看,数据也可以保持一致性;
- 执行事务的过程中,Redis 服务宕机。这里需要考虑服务配置的持久化模式。无持久化的内存模式:服务重启之后,数据库没有保持数据,因此数据都是保持一致性的;RDB / AOF 模式: 服务重启后,Redis 通过 RDB / AOF 文件恢复数据,数据库会还原到一致的状态。
- 《设计数据密集型应用》

- 保证原子性,持久性和隔离性,如果这些特征都无法保证,那么事务的一致性也无法保证;
- 数据库本身的约束,比如字符串长度不能超过列的限制或者唯一性约束;
- 业务层面同样需要进行保障 。
4.5 总结
- 保证隔离性;
- 无法保证持久性;
- 具备了一定的原子性,但不支持回滚;
- 一致性的概念有分歧,假设在一致性的核心是约束的语意下,Redis 的事务可以保证一致性。
5 Lua 脚本
5.1 简介

- 减少网络开销。将多个请求通过脚本的形式一次发送,减少网络时延。
- 原子操作。Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
- 复用。客户端发送的脚本会永久存在 Redis 中,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
5.2 EVAL 命令
EVAL script numkeys key [key ...] arg [arg ...]
script是第一个参数,为 Lua 5.1脚本;
- 第二个参数
numkeys指定后续参数有几个 key;
key [key ...],是要操作的键,可以指定多个,在 Lua 脚本中通过KEYS[1],KEYS[2]获取;
arg [arg ...],参数,在 Lua 脚本中通过ARGV[1],ARGV[2]获取。
redis> eval "return ARGV[1]" 0 100
"100"
redis> eval "return {ARGV[1],ARGV[2]}" 0 100 101
1) "100"
2) "101"
redis> eval "return {KEYS[1],KEYS[2],ARGV[1]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
redis.call()redis> set mystring 'hello world'
OK
redis> get mystring
"hello world"
redis> EVAL "return redis.call('GET',KEYS[1])" 1 mystring
"hello world"
redis> EVAL "return redis.call('GET','mystring')" 0
"hello world"
5.3 EVALSHA 命令

redis> EVALSHA sha1 numkeys key [key ...] arg [arg ...]
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"
5.4 事务 VS Lua 脚本
- 为了避免 Redis 阻塞,Lua 脚本业务逻辑不能过于复杂和耗时;
- 仔细检查和测试 Lua 脚本 ,因为执行 Lua 脚本具备一定的原子性,不支持回滚。
6 实战准备

// 加载 Lua 脚本
String scriptLoad(String luaScript);
// 执行 Lua 脚本
Object eval(String shardingkey,
String luaScript,
ReturnType returnType,
List<Object> keys,
Object... values);
// 通过 sha1 摘要执行Lua脚本
Object evalSha(String shardingkey,
String shaDigest,
List<Object> keys,
Object... values);
public int calcSlot(String key) {
if (key == null) {
return 0;
}
int start = key.indexOf('{');
if (start != -1) {
int end = key.indexOf('}');
key = key.substring(start+1, end);
}
int result = CRC16.crc16(key.getBytes()) % MAX_SLOT;
log.debug("slot {} for {}", result, key);
return result;
}
7 抢红包脚本
- 用户抢红包成功
{
"code":"0",
//红包金额
"amount":"7.1",
//红包编号
"redPacketId":"162339217730846210"
}
- 用户已领取过
{
"code":"1"
}
- 用户抢红包失败
{
"code":"-1"
}
-- KEY[1]: 用户防重领取记录
local userHashKey = KEYS[1];
-- KEY[2]: 运营预分配红包列表
local redPacketOperatingKey = KEYS[2];
-- KEY[3]: 用户红包领取记录
local userAmountKey = KEYS[3];
-- KEY[4]: 用户编号
local userId = KEYS[4];
local result = {};
-- 判断用户是否领取过
if redis.call('hexists', userHashKey, userId) == 1 then
result['code'] = '1';
return cjson.encode(result);
else
-- 从预分配红包中获取红包数据
local redPacket = redis.call('rpop', redPacketOperatingKey);
if redPacket
then
local data = cjson.decode(redPacket);
-- 加入用户ID信息
data['userId'] = userId;
-- 把用户编号放到去重的哈希,value设置为红包编号
redis.call('hset', userHashKey, userId, data['redPacketId']);
-- 用户和红包放到已消费队列里
redis.call('lpush', userAmountKey, cjson.encode(data));
-- 组装成功返回值
result['redPacketId'] = data['redPacketId'];
result['code'] = '0';
result['amount'] = data['amount'];
return cjson.encode(result);
else
-- 抢红包失败
result['code'] = '-1';
return cjson.encode(result);
end
end
- 编写 junit 测试用例 ;
- 从 Redis 3.2 开始,内置了 Lua debugger(简称
LDB), 可以使用 Lua debugger 对 Lua 脚本进行调试。
8 异步任务
- RedisMessageConsumer :消费者类,配置监听队列名,以及对应的消费监听器
String groupName = "userGroup";
String queueName = "userAmountQueue";
RedisMessageQueueBuilder buidler =
redisClient.getRedisMessageQueueBuilder();
RedisMessageConsumer consumer =
new RedisMessageConsumer(groupName, buidler);
consumer.subscribe(queueName, userAmountMessageListener);
consumer.start();
- RedisMessageListener :消费监听器,编写业务消费代码
public class UserAmountMessageListener implements RedisMessageListener {
@Override
public RedisConsumeAction onMessage(RedisMessage redisMessage) {
try {
String message = (String) redisMessage.getData();
// TODO 调用用户余额系统
// 返回消费成功
return RedisConsumeAction.CommitMessage;
}catch (Exception e) {
logger.error("userAmountService invoke error:", e);
// 消费失败,执行重试操作
return RedisConsumeAction.ReconsumeLater;
}
}
}
9 写到最后

边栏推荐
- 10 common website security attack means and defense methods
- Oracle连接MySQL报错IM002
- Test how students participate in codereview
- leetcode:522. Longest special sequence II [greed + subsequence judgment]
- 通俗易懂理解樸素貝葉斯分類的拉普拉斯平滑
- BufferedWriter 和 BufferedReader 的使用
- 【TcaplusDB知识库】Tmonitor后台一键安装介绍(一)
- If you find any loopholes later, don't tell China!
- 技术与业务同等重要,偏向任何一方都是错误
- 2-4 installation of Nessus under Kali
猜你喜欢
随机推荐
leetcode待做题目
Error im002 when Oracle connects to MySQL
R语言plotly可视化:plotly可视化基础小提琴图(basic violin plot in R with plotly)
Evolution of software system architecture
小哥凭“量子速读”绝技吸粉59万:看街景图0.1秒,“啪的一下”在世界地图精准找到!...
[learn FPGA programming from scratch -47]: Vision - current situation and development trend of the third generation semiconductor technology
C# Any()和AII()方法
Tdengine invitation: be a superhero who uses technology to change the world and become TD hero
mysql数据库汉字模糊查询出现异常
【TcaplusDB知识库】Tmonitor单机安装指引介绍(一)
C apprentissage des langues - jour 12.
6月23日《Rust唠嗑室》第三期B站视频地址
R语言plotly可视化:plotly可视化二维直方图等高线图、在等高线上添加数值标签、自定义标签字体色彩、设置鼠标悬浮显示效果(Styled 2D Histogram Contour)
Change PIP mirror source
Brother sucks 590000 fans with his unique "quantum speed reading" skill: look at the street view for 0.1 seconds, and "snap" can be accurately found on the world map
【面经】云泽科技
[从零开始学习FPGA编程-47]:视野篇 - 第三代半导体技术现状与发展趋势
基于swiftadmin极速后台开发框架,我制作了菜鸟教程[专业版]
学习笔记之——数据集的生成
Easy to understand Laplace smoothing of naive Bayesian classification








