百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

Redis的事务,Go+lua用起来真香(redisson事务)

mhr18 2024-11-05 10:24 26 浏览 0 评论

Redis是应对高并发的常用工具,在常用缓存技巧中讲过相关技巧。但有些业务场景,使用Redis会遇到问题,如电商里的秒杀、扣减库存等。

拿减库存举例,一般需要两步:

  • 先扣减库存,获取扣减后的库存值V
  • 如果V小于0,说明库存不够,需要将扣减的值再加回去;如果V大于等于0,则执行后续操作

但这两步是分开的,很可能扣减时成功,但增加回去时失败,导致库存不一致。

另一种方案是:

  • t1时刻,先查询库存,判断是否够用
  • t2时刻,再减库存

但这两步也是分开的,而且t1和t2有时间差,t2时刻扣减库存时,真正的库存和t1时刻已经不一致了。

Redis有没有像MySQL原子性一样的能力,来解决这个问题呢?

事务

要解决扣减库存的问题,可以借助Redis的事务能力。

基本介绍

Redis的基本事务(basic transaction)需要用到MULTI命令和EXEC命令,这种事务可以让一个客户端在不被其他客户端打断的情况下执行多个命令

和关系数据库那种可以在执行的过程中进行回滚(rollback)的事务不同,在Redis里面,被MULTI命令和EXEC命令包围的所有命令会一个接一个地执行,直到所有命令都执行完毕为止。当一个事务执行完毕之后,Redis才会处理其他客户端的命令。

Redis事务在执行的中途遇到错误,不会回滚,而是继续执行后续命令;

  • 还未执行exec就报错:如果事务中出现语法错误,则事务会成功回滚,整个事务中的命令都不会提交
  • 成功执行exec后才报错:如果事务中出现的不是语法错误,而是执行错误,不会触发回滚,该事务中仅有该错误命令不会提交,其他命令依旧会继续提交

另外,Redis里遇到有查询的情况穿插在事务中间,不会返回结果,如下所示,只有执行exec才能返回查询结果:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set a aaa
QUEUED
127.0.0.1:6379(TX)> get a
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) "aaa"

对于事务,做个简单的总结:

  1. Redis事务作为一个整体被执行,执行期间不会被其它客户端打断
  2. Redis事务在执行的中途遇到错误,不会回滚,而是继续执行后续命令
  3. Redis里遇到有查询的情况穿插在事务中间,不会返回结果

Redis事务执行的时候不会被其它客户端打断,所以多个命令可以进行打包,当做一个命令执行。但Redis的原生命令无法提供根据Redis查询结果执行相关动作的功能,这时我们就可以用lua脚本了。

lua

Redis2.6之后新增的功能,我们可以在Redis中通过lua脚本操作Redis。

脚本会将多个命令和操作当成一个命令在Redis中执行,也就是说该脚本在执行的过程中,不会被任何其他脚本或命令打断干扰。

正是因此这种原子性,lua脚本才可以代替multi和exec的事务功能。同时也是因此,在lua脚本中不宜进行过大的开销操作,避免影响后续的其他请求的正常执行。

使用lua脚本的好处:

  • lua脚本是作为一个整体执行的,所以中间不会被其他命令插入
  • 可以把多条命令一次性打包,所以可以有效减少网络开销
  • lua脚本可以常驻在redis内存中,所以在使用的时候,可以直接拿来复用,也减少了代码量

lua脚本在Redis里的样子如下图所示:

实例

场景

秒杀系统里画过秒杀的流程图,其中秒杀库存相关代码为:

 //加库存
	goodsKey := getGoodsKey(p.AppLocal, tag, tid, goods_id)
	cacheQty, err := app.Global().RedisStore.Incr(goodsKey)
	if err != nil {
		return nil, 11008, fmt.Errorf(SERVICE_BUSY)
	}
	//和本次秒杀总数量进行比较
	if cacheQty > int64(goodsInfo.Qty) {
    //不符合条件,则减去库存
		_, err := app.Global().RedisStore.Decr(goodsKey)
		if err != nil {
			ctx.Warn("%s seckill %s ,incr the cnt but not decr success in count process, err:%s", userId, goods_id, err.Error())
		}
		return nil, 11009, fmt.Errorf(NOT_SUCCESS)
	}

可以看出,先增加库存,然后和本次秒杀总数量进行比较,如果超出范围,需减去库存。这种分开的操作,增加了失败的可能性。扣减商品库存,也是类似的逻辑。

使用lua

在Go语言里用lua脚本实现扣减库存操作。我们可以先查询,后比较,最后扣减,脚本将操作打包,代码为:

package main
import (
	"fmt"
	"github.com/go-redis/redis"
)
var Client *redis.Client
func init() {
	Client = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "111111", // no password set
		DB:       0,        // use default DB
	})
}
func useLua() {
	Client.FlushAll()
	//设置初始值
	Client.Set("stock", "10", 0)
	//编写脚本 - 检查数值,是否够用,够用再减,否则返回减掉后的结果
	var luaScript = redis.NewScript(`
		local value = redis.call("Get", KEYS[1])
		print("当前值为 " .. value);
		if( value - KEYS[2] >= 0 ) then
			local leftStock = redis.call("DecrBy" , KEYS[1],KEYS[2])
   			print("剩余值为" .. leftStock );
			return leftStock
		else
			print("数量不够,无法扣减");
			return value - KEYS[2]
		end
		return -1
	`)
	//执行脚本
	n, err := luaScript.Run(Client, []string{"stock", "6"}).Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("结果", n, err)
}
func main() {
	useLua()
}

客户端将lua加载到Redis,脚本被Redis执行:

如果将扣减值设置为25,执行之后,观察到返回值为负数,但再次查询,值未变

? myproject go run main.go

当前值为 10

数量不够,无法扣减

结果 -15

? myproject redis-cli -p 6379 -a 111111

127.0.0.1:6379> get stock

"10"

如果将扣减值设置为6,扣减成功,执行后查看结果

? myproject go run main.go

当前值为 10

剩余值为4

结果 4

? myproject redis-cli -p 6379 -a 111111

127.0.0.1:6379> get stock

"4"

优化lua使用

当脚本会被多次执行时,可考虑使用ScriptLoad和EvalSha代替RUN节省带宽。

  • 先用命令ScriptLoad将脚本缓存到Redis,Redis返回一个sha1的标识符
  • 命令EvalSha基于sha1执行脚本

这种方案只有标识符sha1通过网络传输,而不需传输lua代码块,节省流量,流程如下图所示:

具体代码为:

package main
import (
	"fmt"
	"github.com/go-redis/redis"
)
var Client *redis.Client
var script string = `
		local value = redis.call("Get", KEYS[1])
		print("当前值为 " .. value);
		if( value - KEYS[2] >= 0 ) then
			local leftStock = redis.call("DecrBy" , KEYS[1],KEYS[2])
   			print("剩余值为" .. leftStock );
			return leftStock
		else
			print("数量不够,无法扣减");
			return value - KEYS[2]
		end
		return -1
	`
var luaHash string
func init() {
	Client = redis.NewClient(&redis.Options{
		Addr:     "localhost:6379",
		Password: "111111", // no password set
		DB:       0,        // use default DB
	})
	luaHash, _ = Client.ScriptLoad(script).Result() //返回的脚本会产生一个sha1哈希值,下次用的时候可以直接使用这个值
}
func useLuaHash() {
	n, err := Client.EvalSha(luaHash, []string{"stock", "6"}).Result()
	if err != nil {
		panic(err)
	}
	fmt.Println("结果", n, err)
}
func main() {
	useLuaHash()
}

定制

如果大家觉得在Go中自己写lua不优雅,我们可选择定制方案。

找Redis的运维同学,让运维人员通过lua定制命令,将这些命令常驻Redis内存, 实现复用效果。使用这些定制的命令与使用Redis自身提供的命令没有区别。

当然,前提是运维同学同意做。

问题

虽然使用lua利用Redis事务能力,保证执行过程中不会插入其他命令,但无法解决所有问题:

  • 不确定的返回结果:如请求超时了,要不要归还库存?
    • 这种情况一般不要归还,因为很可能没扣减成功,超卖容易造成资损,少卖问题要小一些
  • 归还失败:扣减成功,但后续操作失败,仍要归还库存,始终有归还失败的可能
    • 这种情况需要重试,而且必须是确定类型的错误才可重试,例如超时就不可重试,同时要做好记录

这也是要求SLA要高,要常记日志的原因,无形中能解决很多问题。

搭建

记录一下在Mac机上搭建Redis的过程,大家可以安装一下Redis,执行Go+lua的代码。

安装

Redis搭建比较简单,使用如下命令即可安装完毕

brew install redis

密码

为了安全,可以设置密码。打开 redis.conf 文件,然后按 command + f 进行搜索:#requirepass foobared 修改为:requirepass 你的密码

服务端启动

执行如下命令,在服务端启动redis

/usr/local/bin/redis-server /usr/local/etc/redis.conf

执行redis-server命令时,如果没有redis.conf文件,则按照默认配置启动,这种情况下登录无需auth

命令行登录

如果设置了auth,命令行登录如下:

redis-cli -p 端口 -a 密码

如果未设置auth,直接执行

redis-cli

总结

有些问题无法百分之百解决,我们要接受这个事实。但并不意味着妥协,我们要能发现问题,确定解决方案,即使是手动解决的方案。有一个前提,解决频率要足够低,这时就没必要把关注点放在100%自动化解决上,产出比特别低的事尽量少做。

资料

  1. 在golang中使用lua脚本
  2. redis登录及设置密码
  3. 为什么说Redis是单线程的以及Redis为什么这么快!
  4. go-redis 事务提交
  5. Golang使用lua脚本实现redis原子操作
  6. Redis事务与Lua
  7. Redis事务的分析及改进
  8. Redis系列(九)、Redis的“事务”及Lua脚本操作

最后

大家如果喜欢我的文章,可以关注我的公众号(程序员麻辣烫)

我的个人博客为:https://shidawuhen.github.io/

往期文章回顾:

  1. 设计模式
  2. 招聘
  3. 思考
  4. 存储
  5. 算法系列
  6. 读书笔记
  7. 小工具
  8. 架构
  9. 网络
  10. Go语言

相关推荐

【推荐】一个开源免费、AI 驱动的智能数据管理系统,支持多数据库

如果您对源码&技术感兴趣,请点赞+收藏+转发+关注,大家的支持是我分享最大的动力!!!.前言在当今数据驱动的时代,高效、智能地管理数据已成为企业和个人不可或缺的能力。为了满足这一需求,我们推出了这款开...

Pure Storage推出统一数据管理云平台及新闪存阵列

PureStorage公司今日推出企业数据云(EnterpriseDataCloud),称其为组织在混合环境中存储、管理和使用数据方式的全面架构升级。该公司表示,EDC使组织能够在本地、云端和混...

对Java学习的10条建议(对java课程的建议)

不少Java的初学者一开始都是信心满满准备迎接挑战,但是经过一段时间的学习之后,多少都会碰到各种挫败,以下北风网就总结一些对于初学者非常有用的建议,希望能够给他们解决现实中的问题。Java编程的准备:...

SQLShift 重大更新:Oracle→PostgreSQL 存储过程转换功能上线!

官网:https://sqlshift.cn/6月,SQLShift迎来重大版本更新!作为国内首个支持Oracle->OceanBase存储过程智能转换的工具,SQLShift在过去一...

JDK21有没有什么稳定、简单又强势的特性?

佳未阿里云开发者2025年03月05日08:30浙江阿里妹导读这篇文章主要介绍了Java虚拟线程的发展及其在AJDK中的实现和优化。阅前声明:本文介绍的内容基于AJDK21.0.5[1]以及以上...

「松勤软件测试」网站总出现404 bug?总结8个原因,不信解决不了

在进行网站测试的时候,有没有碰到过网站崩溃,打不开,出现404错误等各种现象,如果你碰到了,那么恭喜你,你的网站出问题了,是什么原因导致网站出问题呢,根据松勤软件测试的总结如下:01数据库中的表空间不...

Java面试题及答案最全总结(2025版)

大家好,我是Java面试陪考员最近很多小伙伴在忙着找工作,给大家整理了一份非常全面的Java面试题及答案。涉及的内容非常全面,包含:Spring、MySQL、JVM、Redis、Linux、Sprin...

数据库日常运维工作内容(数据库日常运维 工作内容)

#数据库日常运维工作包括哪些内容?#数据库日常运维工作是一个涵盖多个层面的综合性任务,以下是详细的分类和内容说明:一、数据库运维核心工作监控与告警性能监控:实时监控CPU、内存、I/O、连接数、锁等待...

分布式之系统底层原理(上)(底层分布式技术)

作者:allanpan,腾讯IEG高级后台工程师导言分布式事务是分布式系统必不可少的组成部分,基本上只要实现一个分布式系统就逃不开对分布式事务的支持。本文从分布式事务这个概念切入,尝试对分布式事务...

oracle 死锁了怎么办?kill 进程 直接上干货

1、查看死锁是否存在selectusername,lockwait,status,machine,programfromv$sessionwheresidin(selectsession...

SpringBoot 各种分页查询方式详解(全网最全)

一、分页查询基础概念与原理1.1什么是分页查询分页查询是指将大量数据分割成多个小块(页)进行展示的技术,它是现代Web应用中必不可少的功能。想象一下你去图书馆找书,如果所有书都堆在一张桌子上,你很难...

《战场兄弟》全事件攻略 一般事件合同事件红装及隐藏职业攻略

《战场兄弟》全事件攻略,一般事件合同事件红装及隐藏职业攻略。《战场兄弟》事件奖励,事件条件。《战场兄弟》是OverhypeStudios制作发行的一款由xcom和桌游为灵感来源,以中世纪、低魔奇幻为...

LoadRunner(loadrunner录制不到脚本)

一、核心组件与工作流程LoadRunner性能测试工具-并发测试-正版软件下载-使用教程-价格-官方代理商的架构围绕三大核心组件构建,形成完整测试闭环:VirtualUserGenerator(...

Redis数据类型介绍(redis 数据类型)

介绍Redis支持五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sortedset:有序集合)。1、字符串类型概述1.1、数据类型Redis支持...

RMAN备份监控及优化总结(rman备份原理)

今天主要介绍一下如何对RMAN备份监控及优化,这里就不讲rman备份的一些原理了,仅供参考。一、监控RMAN备份1、确定备份源与备份设备的最大速度从磁盘读的速度和磁带写的带度、备份的速度不可能超出这两...

取消回复欢迎 发表评论: