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

Redis弱事务性与Lua脚本原子性分析

suiw9 2024-11-05 12:38 24 浏览 0 评论

1、什么是事务?

简单来说,事务(transaction)是指单个逻辑单元执行的一系列操作。

1.1、事务的四大特性ACID

事务有如下四大特性:

  • 1、原子性(Atomicity): 构成事务的所有操作都必须是一个逻辑单元,要么全部执行,要么全不执行
  • 2、一致性(Consistency): 数据库在事务执行前后状态都必须是稳定的或者一致的。A(1000)给B(200)转账100后A(900),B(300)总和保持一致。
  • 3、隔离性(Isolation): 事务之间相互隔离,互不影响。
  • 4、持久性(Durability): 事务执行成功后数据必须写入磁盘,宕机重启后数据不会丢失。

2、Redis中的事务

Redis中的事务通过multi,exec,discard,watch这四个命令来完成。

Redis的单个命令都是原子性的,所以确保事务的就是多个命令集合一起执行。

Redis命令集合打包在一起,使用同一个任务确保命令被依次有序且不被打断的执行,从而保证事务性。

Redis是弱事务,不支持事务的回滚。

2.1、事务命令

事务命令简介

  • 1、multi(开启事务)
    • 用于表示事务块的开始,Redis会将后续的命令逐个放入队列,然后使用exec后,原子化的执行这个队列命令。
    • 类似于mysql事务的begin
  • 2、exec(提交事务)
    • 执行命令队列
    • 类似于mysql事务的commit
  • 3、discard(清空执行命令)
    • 清除命令队列中的数据
    • 类似于mysql事务的rollback,但与rollback不一样 ,这里是直接清空队列所有命令,从而不执行。所以不是的回滚。就是个清除。
  • 4、watch
    • 监听一个redis的key 如果key发生变化,watch就能后监控到。如果一个事务中,一个已经被监听的key被修改了,那么此时会清空队列。
  • 5、unwatch
    • 取消监听一个redis的key

事务操作

# 普通的执行多个命令
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name zhangsan
QUEUED
127.0.0.1:6379> hmset m_set name zhangsan age 20 
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK

# 执行命令前清空队列 将会导致事务执行不成功
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name_1 lisi
QUEUED
127.0.0.1:6379> hmset m_set_1 name lisi age 21
QUEUED
# 提交事务前执行了清空队列命令
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI

# 监听一个key,并且在事务提交之前改变在另一个客户端改变它的值,也会导致事务失败
127.0.0.1:6379> set m_name_2 wangwu01
OK
127.0.0.1:6379> watch m_name_2
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set m_name_2 wangwu02
QUEUED
# 另外一个客户端在exec之前执行之后,这里会返回nil,也就是清空了队列,而不是执行成功
127.0.0.1:6379> exec
(nil)

# 另外一个客户端在exec之前执行
127.0.0.1:6379> set m_name_2 niuqi
OK

2.2、事务机制分析

我们前面总是在说,Redis的事务命令是打包放在一个队列里的。那么来看一下Redis客户端的数据结构吧。

client数据结构

typedef struct client {
    // 客户端唯一的ID
    uint64_t id;   
    // 客户端状态 表示是否在事务中
    uint64_t flags;         
    // 事务状态
    multiState mstate;
    // ...还有其他的就不一一列举了
} client;

multiState事务状态数据结构

typedef struct multiState {
    // 事务队列 是一个数组,按照先入先出顺序,先入队的命令在前 后入队的命令在后
    multiCmd *commands;     /* Array of MULTI commands */
    // 已入队命令数
    int count;              /* Total number of MULTI commands */
    // ...略
} multiState;

multiCmd事务命令数据结构

/* Client MULTI/EXEC state */
typedef struct multiCmd {
    // 命令的参数
    robj **argv;
    // 参数长度
    int argv_len;
    // 参数个数
    int argc;
    // redis命令的指针
    struct redisCommand *cmd;
} multiCmd;

Redis的事务执行流程图解

Redis的事务执行流程分析

  • 1、事务开始时,在Client中,有属性flags,用来表示是否在事务中,此时设置flags=REDIS_MULTI
  • 2、Client将命令存放在事务队列中,事务本身的一些命令除外(EXEC,DISCARD,WATCH,MULTI)
  • 3、客户端将命令放入multiCmd *commands,也就是命令队列
  • 4、Redis客户端将向服务端发送exec命令,并将命令队列发送给服务端
  • 5、服务端接受到命令队列后,遍历并一次执行,如果全部执行成功,将执行结果打包一次性返回给客户端。
  • 6、如果执行失败,设置flags=REDIS_DIRTY_EXEC, 结束循环,并返回失败。

2.3、监听机制分析

我们知道,Redis有一个expires的字典用于key的过期事件,同样,监听的key也有一个类似的watched_keys字典,key是要监听的key,值是一个链表,记录了所有监听这个key的客户端。

而监听,就是监听这个key是否被改变,如果被改变了,监听这个key的客户端的flags属性就设置为REDIS_DIRTY_CAS。

Redis客户端向服务器端发送exec命令,服务器判断Redis客户端的flags,如果为REDIS_DIRTY_CAS,则清空事务队列。

redis监听机制图解

redis监听key数据结构

回过头再看一下RedisDb类的watched_keys,确实是一个字典,数据结构如下:

typedef struct redisDb {
    dict *dict;                 /* 存储所有的key-value */
    dict *expires;              /* 存储key的过期时间 */
    dict *blocking_keys;        /* blpop存储阻塞key和客户端对象*/
    dict *ready_keys;           /* 阻塞后push,响应阻塞的那些客户端和key */
    dict *watched_keys;         /* 存储watch监控的key和客户端对象 WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* 数据库的ID为0-15,默认redis有16个数据库 */
    long long avg_ttl;          /* 存储对象的额平均ttl(time in live)时间用于统计 */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
    clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

2.4、Redis的弱事务性

为什么说Redis是弱事务性呢? 因为如果redis事务中出现语法错误,会暴力的直接清除整个队列的所有命令。

# 在事务外设置一个值为test
127.0.0.1:6379> set m_err_1 test
OK
127.0.0.1:6379> get m_err_1 
"test"
# 开启事务 修改值 但是队列的其他命令出现语法错误  整个事务会被discard
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> set m_err_1 test1
QUEUED
127.0.0.1:6379> sets m_err_1 test2
(error) ERR unknown command `sets`, with args beginning with: `m_err_1`, `test2`, 
127.0.0.1:6379> set m_err_1 test3
QUEUED
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.

# 重新获取值
127.0.0.1:6379> get m_err_1
"test"

我们发现,如果命令队列中存在语法错误,是直接的清除队列的所有命令,并不是进行事务回滚,但是语法错误是能够保证原子性的

再来看一些,如果出现类型错误呢?比如开启事务后设置一个key,先设置为string, 然后再当成列表操作。

# 开启事务
127.0.0.1:6379> multi 
OK
# 设置为字符串
127.0.0.1:6379> set m_err_1 test_type_1
QUEUED
# 当初列表插入两个值
127.0.0.1:6379> lpush m_err_1 test_type_1 test_type_2
QUEUED
# 执行
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of valu
# 重新获取值,我们发现我们的居然被改变了,明明,事务执行失败了啊
127.0.0.1:6379> get m_err_1
"test_type_1"

直到现在,我们确定了redis确实不支持事务回滚。因为我们事务失败了,但是命令却是执行成功了。

弱事务总结

  • 1、大多数的事务失败都是因为语法错误(支持回滚)或者类型错误(不支持回滚),而这两种错误,再开发阶段都是可以遇见的
  • 2、Redis为了性能,就忽略了事务回滚。

那么,redis就没有办法保证原子性了吗,当然有,Redis的lua脚本就是对弱事务的一个补充。

3、Redis中的lua脚本

lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua应用场景:游戏开发、独立应用脚本、Web应用脚本、扩展和数据库插件。

OpenResty:一个可伸缩的基于Nginx的Web平台,是在nginx之上集成了lua模块的第三方服务器。

OpenResty是一个通过Lua扩展Nginx实现的可伸缩的Web平台,内部集成了大量精良的Lua库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发(日活千万级别)、扩展性极高的动态Web应用、Web服务和动态网 关。 功能和nginx类似,就是由于支持lua动态脚本,所以更加灵活,可以实现鉴权、限流、分流、日志记 录、灰度发布等功能。

OpenResty通过Lua脚本扩展nginx功能,可提供负载均衡、请求路由、安全认证、服务鉴权、流量控 制与日志监控等服务。

类似的还有Kong(Api Gateway)、tengine(阿里)

3.1、Lua安装(Linux)

lua脚本下载和安装http://www.lua.org/download.html

lua脚本参考文档:http://www.lua.org/manual/5.4/

# curl直接下载
curl -R -O http://www.lua.org/ftp/lua-5.4.4.tar.gz
# 解压
tar zxf lua-5.4.4.tar.gz
# 进入,目录
cd lua-5.4.4
# 编译安装
make all test

编写lua脚本

编写一个lua脚本test.lua,就定义一个本地变量,打印出来即可。

local name = "zhangsan"

print("name:",name)

执行lua脚本

[root@VM-0-5-centos ~]# lua test.lua 
name:   zhangsan

3.2、Redis中使用Lua

Redis从2.6开始,就内置了lua编译器,可以使用EVAL命令对lua脚本进行求值。

脚本命令是原子性的,Redis服务器再执行脚本命令时,不允许新的命令执行(会阻塞,不在接受命令)。、

EVAL命令

通过执行redis的eval命令,可以运行一段lua脚本。

EVAL script numkeys key [key ...] arg [arg ...]

EVAL命令说明

  • 1、script:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
  • 2、numkeys:指定键名参数的个数。
  • 3、key [key ...]:从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中所用到的哪些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)
  • 4、arg [arg ...]:可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似(ARGV[1] 、 ARGV[2] ,诸如此类)

简单来说,就是

eval lua脚本片段  参数个数(假设参数个数=2)  参数1 参数2  参数1值  参数2值

EVAL命令执行

# 执行一段lua脚本 就是把传入的参数和对应的值返回回去
127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 name age zhangsan 20
1) "name"
2) "age"
3) "zhangsan"
4) "20"

lua脚本中调用redis

我们直到了如何接受和返回参数了,那么lua脚本中如何调用redis呢?

  • 1、redis.call
    • 返回值就是redis命令执行的返回值
    • 如果出错,则返回错误信息,不继续执行
  • 2、redis.pcall
    • 返回值就是redis命令执行的返回值
    • 如果出错,则记录错误信息,继续执行

其实就是redis.call会把异常抛出来,redis.pcall则时捕获了异常,不会抛出去。

lua脚本调用redis设置值

# 使用redis.call设置值
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 eval_01 001
OK
127.0.0.1:6379> get eval_01
"001"

EVALSHA命令

前面的eval命令每次都要发送一次脚本本身的内容,从而每次都会编译脚本。

Redis提供了一个缓存机制,因此不会每次都重新编译脚本,可能在某些场景,脚本传输消耗的带宽可能是不必要的。

为了减少带宽的西消耗,Redis实现了evaklsha命令,它的作用和eval一样,只是它接受的第一个参数不是脚本,而是脚本的SHA1校验和(sum)。

所以如何获取这个SHA1的值,就需要提到Script命令。

  • 1、SCRIPT FLUSH :清除所有脚本缓存。
  • 2、SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存。
  • 3、SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它。
  • 4、SCRIPT KILL :杀死当前正在运行的脚本

执行evalsha命令

# 使用script load将脚本内容加载到缓存中,返回sha的值
127.0.0.1:6379> script load "return redis.call('set',KEYS[1],ARGV[1])"
"c686f316aaf1eb01d5a4de1b0b63cd233010e63d"
# 使用evalsha和返回的sha的值 + 参数个数 参数名称和值执行
127.0.0.1:6379> evalsha c686f316aaf1eb01d5a4de1b0b63cd233010e63d 1 eval_02 002
OK
# 获取结果
127.0.0.1:6379> get eval_02
"002"

我们上面都是将脚本写在代码行里面,可以不可以将脚本内容写在xxx.lua中,直接执行呢? 当然是可以的。

使用redis-cli运行外置lua脚本

编写外置脚本test2.lua, 设置值到redis中。

# 脚本内容 也就是设置一个值
return redis.call('set',KEYS[1],ARGV[1])

# 执行结果,可以使用./redis-cli -h 127.0.0.1 -p 6379 指定redis ip、端口等
root@62ddf68b878d:/data# redis-cli --eval /data/test2.lua eval_03 , test03       
OK

利用Redis整合lua脚本,主要是为了保证性能是事务的原子性,因为redis的事务功能确实有些差劲!

4、Redis的脚本复制

Redis如果开启了主从复制,脚本是如何从主服务器复制到从服务器的呢?

首先,redis的脚本复制有两种模式,脚本传播模式和命令传播模式。

在开启了主从,并且开启了AOF持久化的情况下。

4.1、脚本传播模式

其实就是主服务器执行什么脚本,从服务器就执行什么样的脚本。但是如果有当前事件,随机函数等会导致差异。

主服务器执行命令

# 执行多个redis命令并返回
127.0.0.1:6379> eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002
1) OK
2) OK
127.0.0.1:6379> get eval_test_01
"0001"
127.0.0.1:6379> get eval_test_02
"0002"

那么主服务器将向从服务器发送完全相同的eval命令:

eval "local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_01 eval_test_02 0001 0002

注意:在这一模式下执行的脚本不能有时间、内部状态、随机函数等。执行相同的脚本以及参数必须产生相同的效果。在Redis5,也是处于同一个事务中。

4.2、命令传播模式

处于命令传播模式的主服务器会将执行脚本产生的所有写命令用事务包裹起来,然后将事务复制到AOF文件以及从服务器里面.

因为命令传播模式复制的是写命令而不是脚本本身,所以即使脚本本身包含时间、内部状态、随机函数等,主服务器给所有从服务器复制的写命令仍然是相同的。

为了开启命令传播模式,用户在使用脚本执行任何写操作之前,需要先在脚本里面调用以下函数:

redis.replicate_commands()

redis.replicate_commands() 只对调用该函数的脚本有效:在使用命令传播模式执行完当前脚本之后,服务器将自动切换回默认的脚本传播模式。

执行脚本

eval "redis.replicate_commands();local result1 = redis.call('set',KEYS[1],ARGV[1]); local result2 = redis.call('set',KEYS[2],ARGV[2]); return {result1, result2}" 2 eval_test_03 eval_test_04 0003 0004

appendonly.aof文件内容

*1
$5
MULTI
*3
$3
set
$12
eval_test_03
$4
0003
*3
$3
set
$12
eval_test_04
$4
0004
*1
$4
EXEC

可以看到,在一个事务里面执行了我们脚本执行的命令。

同样的道理,主服务器只需要向从服务器发送这些命令就可以实现主从脚本数据同步了。

5、Redis的管道/事务/脚本

  • 1、管道其实就是一次性执行一批命令,不保证原子性,命令都是独立的,属于无状态操作(也就是普通的批处理)
  • 2、事务和脚本是有原子性的,但是事务是弱原子性,lua脚本是强原子性。
  • 3、lua脚本可以使用lua语言编写比较复杂的逻辑。
  • 4、lua脚本的原子性强于事务,脚本执行期间,另外的客户端或其他的任何脚本或命令都无法执行。所以lua脚本的执行事件应该尽可能的短,不然会导致redis阻塞不能做其他工作。

6、小结

Redis的事务是弱事务,多个命令开启事务一起执行性能比较低,且不能一定保证原子性。所以lua脚本就是对它的补充,它主要就是为了保证redis的原子性。

比如有的业务(接口Api幂等性设计,生成token,(取出toker并判断是否存在,这就不是原子操作))我们需要获取一个key, 并且判断这个key是否存在。就可以使用lua脚本来实现。

还有很多地方,我们都需要redis的多个命令操作需要保证原子性,此时lua脚本可能就是一个不二选择。

7、相关文章

本人还写了Redis的其他相关文章,有兴趣的可以点击查看!

相关推荐

俄罗斯的 HTTPS 也要被废了?(俄罗斯网站关闭)

发布该推文的ScottHelme是一名黑客,SecurityHeaders和ReportUri的创始人、Pluralsight作者、BBC常驻黑客。他表示,CAs现在似乎正在停止为俄罗斯域名颁发...

如何强制所有流量使用 HTTPS一网上用户

如何强制所有流量使用HTTPS一网上用户使用.htaccess强制流量到https的最常见方法可能是使用.htaccess重定向请求。.htaccess是一个简单的文本文件,简称为“.h...

https和http的区别(https和http有何区别)

“HTTPS和HTTP都是数据传输的应用层协议,区别在于HTTPS比HTTP安全”。区别在哪里,我们接着往下看:...

快码住!带你十分钟搞懂HTTP与HTTPS协议及请求的区别

什么是协议?网络协议是计算机之间为了实现网络通信从而达成的一种“约定”或“规则”,正是因为这个“规则”的存在,不同厂商的生产设备、及不同操作系统组成的计算机之间,才可以实现通信。简单来说,计算机与网络...

简述HTTPS工作原理(简述https原理,以及与http的区别)

https是在http协议的基础上加了一层SSL(由网景公司开发),加密由ssl实现,它的目的是为用户提供对网站服务器的身份认证(需要CA),以至于保护交换数据的隐私和完整性,原理如图示。1、客户端发...

21、HTTPS 有几次握手和挥手?HTTPS 的原理什么是(高薪 常问)

HTTPS是3次握手和4次挥手,和HTTP是一样的。HTTPS的原理...

一次安全可靠的通信——HTTPS原理

为什么HTTPS协议就比HTTP安全呢?一次安全可靠的通信应该包含什么东西呢,这篇文章我会尝试讲清楚这些细节。Alice与Bob的通信...

为什么有的网站没有使用https(为什么有的网站点不开)

有的网站没有使用HTTPS的原因可能涉及多个方面,以下是.com、.top域名的一些见解:服务器性能限制:HTTPS使用公钥加密和私钥解密技术,这要求服务器具备足够的计算能力来处理加解密操作。如果服务...

HTTPS是什么?加密原理和证书。SSL/TLS握手过程

秘钥的产生过程非对称加密...

图解HTTPS「转」(图解http 完整版 彩色版 pdf)

我们都知道HTTPS能够加密信息,以免敏感信息被第三方获取。所以很多银行网站或电子邮箱等等安全级别较高的服务都会采用HTTPS协议。...

HTTP 和 HTTPS 有何不同?一文带你全面了解

随着互联网时代的高速发展,Web服务器和客户端之间的安全通信需求也越来越高。HTTP和HTTPS是两种广泛使用的Web通信协议。本文将介绍HTTP和HTTPS的区别,并探讨为什么HTTPS已成为We...

HTTP与HTTPS的区别,详细介绍(http与https有什么区别)

HTTP与HTTPS介绍超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的...

一文让你轻松掌握 HTTPS(https详解)

一文让你轻松掌握HTTPS原文作者:UC国际研发泽原写在最前:欢迎你来到“UC国际技术”公众号,我们将为大家提供与客户端、服务端、算法、测试、数据、前端等相关的高质量技术文章,不限于原创与翻译。...

如何在Spring Boot应用程序上启用HTTPS?

HTTPS是HTTP的安全版本,旨在提供传输层安全性(TLS)[安全套接字层(SSL)的后继产品],这是地址栏中的挂锁图标,用于在Web服务器和浏览器之间建立加密连接。HTTPS加密每个数据包以安全方...

一文彻底搞明白Http以及Https(http0)

早期以信息发布为主的Web1.0时代,HTTP已可以满足绝大部分需要。证书费用、服务器的计算资源都比较昂贵,作为HTTP安全扩展的HTTPS,通常只应用在登录、交易等少数环境中。但随着越来越多的重要...

取消回复欢迎 发表评论: