本文主要是本人学习Redis内容过程中重点学习内容,无论是面试还是日常工作都有很大作用。自己也会不定时继续学习Redis并且补充内容。内容基本来自于<<Redis设计与实现>>,有空会看更多内容,希望大家推荐
1、redis简介
2、数据库发展 redis:采用聚合数据结构存储
3、适合场景:频繁访问、数据量小的数据库(缓存数据库)
redis数据结构简单总结:
- 动态字符串(SDS)
在c语言的string上做了一层封装,可以类比java的String,结构如下
struct sdshdr {
int free --记录buf中未使用字节数量
int len --记录buf中已使用字节数量
char buf --字节数组,用于保存字符串
}
一些功能:
1、空间预分配
对于小于1MB的字符串,在分配buf时会分配两倍的内存
对于大于1MB的字符串,在分配buf时会多分配1MB的空间
2、惰性空间释放
用free来记录当前删除字节数量
3、二进制安全
2. 链表--LINKEDLIST
和普通的链表一样,这里就不在多叙述了,给出结构
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
// 节点的值, 这里设置成空指针,可以保存不同类型的值
void *value
} listNode;
typedef struct list {
listNode *head;
listNode *tail;
unsigned long len;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
} list;
3. 字典---HT
可以理解为哈希表,实现类型和java中的map类似,这里给出结构
哈希表:
typedef struct dictht {
// 哈希表数组,里面每个元素是dictEntry指针
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,计算索引用的
unsigned long sizemask;
// 哈希表现在已有节点数量
unsigned long used;
} dictht;
typedef struct dictEntry {
// 键
void *key;
// 值
uion {
void *val; // 值可以是指针
uint64_tu64; // 也可以是一个uint64_t整数
int64_ts64; // 也可以是个int64_t整数
} v;
// 指向下一个哈希表节点,链地址法解决哈希冲突
struct dictEntry *next;
} dictEntry;
字典:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表,两个的原因是因为要用一个来扩容, 扩容时会使用h[1], 一般只使用h[0]
dictht ht[2];
// rehash索引,当不在rehash时,值为-1
int trehashidx;
}dict;
一些不同点:
渐进式rehash
服务器不是一次性将ht[0]里面所有键值全部rehash,具体步骤如下
将rehashidx设置为0,表示开始rehash工作
每次对字典进行增删改查时,不仅会执行这些操作,还会对hash到的table做rehash,完成后将rehashidx+1
最终所有ht[0]所有键值都会rehash到ht[1]上
在rehash过程中,如果出现查找删除修改操作,会在h[0], h[1]上寻找,如果只有增加键,那么只会在h[1]上增加键
4. 跳跃表---SKIPLIST
跳跃表已经在lsm树引擎的memtable中实现,这里不再重复
多了一个分值(score),各节点按照所保存的分值从小到大排序 在同一个跳表中,会先按照分值进行排序,如果分值相同则按照成员对象在字典序中的大小来排序有一个跳表结构体来持有跳表结点:
typedef struct zskiplist {
// 跳表头节点和尾节点
struct zskiplistNode *header, *tail;
// 表中节点数量
unsigned long length;
//表中层数最大的结点的层数
int level;
} zskiplist;
跳表节点结构:
typedef struct zskiplistNode {
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplist *forward;
//跨度
unsigned int span;
} level[];
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
} zskiplistNode;
5. 整数集合---整数集合
整数集合是redis用于保存整数值的集合抽象数据结构,可以保存int16_t, int32_t, int64_t,并且集合中不会出现重复元素
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合长度
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
INTSET_ENC_INT16 --- int16_t
INTSET_ENC_INT32 --- int32_t
INTSET_ENC_INT64 --- int64_t
关键概念:升级
当新加元素的类型要比原数组类型更大时,整数集合需要先升级,再添加。
1、根据新元素的类型,扩展底层数组的空间大小,并为新元素分配空间
2、将底层数组所有元素转成新元素类型,放入新数组中,要保持有序
3、添加新元素
6. 压缩列表---ZIPLIST
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者一个整数值。
// 类似结构:
typedef struct zip {
// 记录整个压缩列表占用的内存字节数
uint32_t zlbytes;
// 记录压缩列表尾结点距压缩列表起始地址有多少字节
uint32_t zltail;
// 节点数量
uint16_t zllen;
// 节点数组
value entryX[];
// 标记压缩列表的末端
uint8_t zlend;
} zip;
节点组成:字节数组或者整数值
字节数组:
长度小于等于63(2^6 - 1)字节的字节数组
长度小于等于16383(2^14 - 1)字节的字节数组
长度小于等于4294967295(2^32 - 1)字节的字节数组
整数值:
4位长,介于0~12间的无符号整数
1字节长的有符号整数
3字节长的有符号整数
int16_t类型整数
int32_t类型整数
int64_t类型整数
节点结构:previous_entry_length + encoding + contents
previous_entry_length: 记录了压缩列表中前一个结点的长度,一字节或者五字节。
如果前一个节点长度小于254,那么为一个字节
如果前一个结点长度大于等于254,那么长度为5字节,其中第一个字节被设置为0xFE(254),后四个字节为实际长度
encoding: 记录了结点的content属性所保存数据的类型及长度。
最高位00开头,1字节长:保存着长度小于等于63字节的字节数组
最高位01开头,2字节长:保存着长度小于等于16383字节的字节数组
最高位10开头,5字节长:保存着长度小于等于4294967295字节的字节数组
最高位11开头,1字节长:保存着整数值
7、对象
总体概括:每当我们在Redis数据库中新创建一个键值对时,会至少创建两个对象,一个为键,另一个为值
Redis中所有对象都由redisObject结构表示:
typedef struct redisObject {
// 类型
unsigned type: 4;
//编码
unsigned encoding: 4;
// 指向底层实现数据结构的指针
void *ptr;
// ...
} robj;
类型:对于键,总是一个字符串对象,而值则可以是其余五种对象的一种
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象
编码:表示了这个对象使用了什么数据结构作为对象的底层实现
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 双端链表
REDIS_ENCODING_ZIPLIST 压缩列表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表和字典
接下来介绍五种对象和对应的底层实现
1、字符串对象 字符串对象的编码可以是int,embstr或者raw
2、列表对象 列表对象的编码可以是ziplist或者linkedlist
3、哈希对象 哈希对象的编码可以是ziplist或者hashtable
4、集合对象 集合对象的编码可以是intest或者hashtable
5、有序集合对象(ZSET) 有序集合对象的编码可以是ziplist或者skiplist
有序集合的skipList编码中,选择同时使用跳跃表和字典实现。其中: 字典负责保存每个键的分值,即:key为元素成员,value为元素分值。 skiplist保存成员,保证成员的有序性。 两中数据结构通过指针来共享相同元素的成员和分值,因此不会浪费额外内存。 选择这样实现的原因:字典能保证查找score时间复杂度为O(1), skiplist能保证元素的有序性,在执行范围操作时可以以o(n)复杂度查找
有序集合对象使用ziplist编码的条件(同时满足):
- 有序集合保存的元素小于128个
- 有序集合保存的所有元素成员长度都小于64字节。
其余情况使用skiplist编码。
内存回收:
Redis的对象系统中使用引用计数的方式实现内存回收机制。在redisObject结构中的refcount属性中记录。
当在创建一个新对象时,引用计数的值会被初始化为1
当对象被一个新程序使用时,它的引用计数值会+1
当对象不再被一个程序使用时,它的引用计数值会-1
当对象的引用计数值变0时,占用内存会被释放。
注:这些回收逻辑核Netty中的bytebuf很像,bytebuf对象也是实现了引用计数回收
同时,对象的引用计数属性还带有对象共享的作用。
8、数据库键空间
redisDB结构体中的dict字典保存了数据库中的所有键值对,这个字典被称为键空间 键空间和用户缩减的数据库是直接对应的:
键空间的键也就是数据库的键,每个键是一个字符串对象 键空间的值就是数据库的值,值为上面提到的五种对象任意一种。
一些注意点:
1. FLUSHDB是通过删除键空间的所有键值对实现的。RANDOMKEY命令是通过在键空间随机返回一个键实现的,具体怎么实现随机返回可以参考
2. 在读取一个键后,会更新服务器键空间的命中次数或者不命中次数
3. 在读取一个键后,会更新键的LRU时间。
4. 在读取一个键时,如果发现该键已过期,那么服务器会先删除过期键。
5. 如果有WATCH命令监视了某个键,那么会标记这个键为脏,通过事件通知
6. 每次修改一个键,都会对脏键计数器的值增1,这个计数器会触发服务器的持久化或复制操作。
redisDb结构体中的expires字典保存了数据库中所有键的过期时间,称之为过期字典。
过期字典的键是一个指针,指向键空间的某个键对象
过期字典的值是一个long long类型的整数,这个整数保存了键的过期时间(毫秒进度的UNIX时间戳)
总结:
1.Redis服务器的所有数据库都保存在redisServer.db数组中,而数据库的数量则由redisserver.dbnum属性保存。
2.客户端通过修改目标数据库指针,让它指向redisServer. db数组中的不同元素来切换不同的数据库。
3.数据库主要由dict和 expires两个字典构成,其中dict字典负责保存键值对,而expires字典则负责保存键的过期时间。
4.因为数据库由字典构成,所以对数据库的操作都是建立在字典操作之上的。
5.数据库的键总是一个字符串对象,而值则可以是任意一种 Redis对象类型,包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象,分别对应字符串键、哈希表键、集合键、列表键和有序集合键。
6.expires字典的键指向数据库中的某个键,而值则记录了数据库键的过期时间,过期时间是一个以毫秒为单位的UNIX时间截。
7.Redis使用惰性删除和定期删除两种策略来删除过期的键:惰性删除策略只在碰到过期键时才进行删除操作,定期删除策略 则每隔一段时间主动查找并删除过期键。
8.执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已经过期的键。
9.执行BGREWRITEAOF命令所产生的重写AOF文件不会包含已经过期的键。
10.当一个过期键被删除之后,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键。
11.当主服务器删除一个过期键之后,它会向所有从服务器发送一条DEL命令,显式地删除过期键。
12.从服务器即使发现过期键也不会自作主张地删除它,而是等待主节点发来DEL命令,这种统一、中心化的过期键删除策略 可以保证主从服务器数据的一致性。
13.当Redis命令对数据库进行修改之后,服务器会根据配置向客户端发送数据库通知。
9、RDB
RDB文件用于保存和还原Redis服务器中所有键值对数据。
SAVE命令由服务器进程直接执行保存操作,会阻塞服务器
BGSAVE由子进程执行保存操作,不会阻塞服务器
RDB文件的创建:主要介绍BGSAVE(每隔100毫秒会检查一次是否执行),代码如下:
bgsave原理是fork() + copyonwrite blog.csdn.net/ymb615ymb/a…
def BGSAVE():
// 创建子进程
pid = fork()
if pid == 0:
// 子进程负责创建RDB文件
rdbSave()
// 完成之后向父进程发送信号
signal_parent()
elif pid > 0:
// 父进程继续处理命令请求,并通过轮询等待子进程信号
handle_request_and_wait_signal()
注意点:服务器优先使用AOF还原数据库
BGSAVE采用了copyonwrite策略:
如有多个调用者同时请求相同资源(内存或磁盘上数据),他们会共同获取相同指针指向相同的资源,直到某个调用者试图修改资源时,
系统才真正复制一份专用副本给该调用者,其他调用者所见最初资源仍不变。
CopyOnWrite:就是不同线程,操作不同资源,再整合,因此线程安全,但不能保证强一致性
10、AOF(Append Only File)
被写入AOF的所有命令都是以Redis的命令请求协议格式保存的。在AOF文件里,除了用于指定数据库的select命令是服务器自动添加的外,其余都是通过客户端发送的命令
实现:可以分为命令追加,文件写入,文件同步。
命令追加:当服务器在执行完一个写命令后,会以协议格式将被执行的命令追加到服务器状态的aof_buf缓冲区末尾
文件写入:当服务器每次结束一个事件循环之前,它都会调用flushAppendOnlyFile函数,将aof_buf缓冲区的内容写入和保存到AOF文件中
文件载入和数据还原:从AOF文件读取一条写命令,然后执行,以此往复
AOF重写:随着服务器运行,AOF文件内容会越来越多,为了解决AOF文件体积膨胀,使用了AOF文件重写的功能。即新建一个AOF,删除原有AOF
具体实现:首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录的多条命令。命令为BGREWRITEAOF。
redis将aof重写程序放到子进程中执行,这样父进程可以继续处理命令请求。为了解决数据不一致,Redis设置了一个AOF重写缓冲区(开启子进程时),当Redis服务器执行完一个写命令后,同时会将这个写命令发送给AOF缓冲区和AOF重写缓冲区。这样,现有AOF也可以被同步,同时重建AOF也会被刷新。当子进程完成重写工作后,会通过信号通知父进程调用处理函数,将AOF重写缓冲区的所有内容写入新AOF中,同时原子的覆盖现有AOF文件。
11、Redis中事件
调度规则:
1:获取到达时间离当前时间最接近的时间事件
2:计算最接近的时间事件距离到达还有多少毫秒,求出差值
2.1:如果差值小于等于0,代表此时已经有时间时间发生了
2.1.1:此时会调用aeApiPoll后马上返回,不阻塞
2.2:如果差值大于0,说明还未发生时间事件,此时aeApiPoll会阻塞到差值时间
3:处理所有已经产生的文件事件
4:处理所有已经到达的时间事件
对文件事件和时间事件的处理都是同步、有序、原子地执行的,服务器不会中途中断事件处理,也不会对事件进行抢占。因此,对于handler,它们都会
尽可能地减少程序的阻塞时间,并在有需要时主动让出执行权。比如:如果回复处理器写入回复内容超过预设值时,会主动break跳出写入循环,余下内容
留到下一次再写入
注:规则可以类比Netty中的NioEventLoop的run方法,做法很类似
Redis会为每个客户端维护一个redisClient结构,这些结构组成一个链表,保存所有客户端的状态。有点和netty的NiosocketChannel类似,对原生io的封装
里面的属性大概如下:
typedef struct redisClient {
// 套接字描述符
int fd;
// 名字
robj *name;
//标识, 标志着这个客户端是什么客户端,用bit位表示
int flags;
// 输入缓冲区,用于保存客户端发送的命令请求,同时会根据内容动态地缩小或者扩大,最大不超过1G,否则会关闭客户端连接。
sds querybuf;
// 命令与命令参数
// 一个数组,每个项都是一个字符串对象,其中argv[0]是要执行的命令,其他是参数
robj **argv;
// 记录argv长度
int argc;
// 命令的实现函数,为一个字典,键为SDS,保存命令名字,值为对应的redisCommand结构,保存了命令实现函数,标志,参数个数,执行次数,消耗时长等
struct redisCommand *cmd;
// 输出缓冲区,每个客户端都有两个,一个是固定长度,一个是可变长度
// 固定长度buf
//char buf[REDIS_REPLY_CHUNK_BYTES];
int bufpos;
// 可变长度buf,由多个字符串对象组成
list *reply;
// 身份验证
int authenticated;
} redisClient;
12、redis主从同步
完整同步操作:
从服务器发送SYNC命令
收到后主服务器执行BGSAVE,生成RDB文件,并用缓冲区记录现在开始的所有写命令。
将RDB文件发送给从服务器同步
将缓冲区的命令发送给从服务器同步
三个部分:
复制偏移量
复制积压缓冲区
服务器的运行id
复制偏移量:
主服务器每次从服务器传播N个字节的数据时,就将自己的复制偏移量+N ---- 没有使用法定人数更新
从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量+N
复制积压缓冲区:
由主服务器维护的一个固定长度先进先出队列,大小默认为1MB。
命令传播时,会将命令写入缓冲区中,再发送给其他从服务器,还会保存着相对应的复制偏移量(一部分最近的)
如果偏移量之后的数据仍然存在与复制积压缓冲区中,那么主服务器对从服务器执行部分同步操作。
如果不在,那么主服务器将对从服务器执行完整同步操作。
根据需要调整复制积压缓冲区大小。保证绝大部分断线情况都能用部分同步来处理。
服务器运行id:
如果恢复后服务器id和原先相同,就可以执行部分同步操作。否则会使用完整同步操作
实现步骤:
1.设置主服务器的端口和地址
将主服务器IP地址和端口号保存在服务器状态的masterhost属性和masterport属性中。
struct redisServer {
// ...
// 主服务器的地址
char *masterhost;
// 主服务器的端口
int masterport;
// ...
}
2.建立套接字连接
主服务器将从服务器套接字创建相对应的客户端状态。
3.发送ping命令
从服务器向主服务器发送PING命令,主服务器返回PONG命令,保证主从服务器之间的网络连接状态正在。
4.身份验证
如果从服务器设置了masterauth选项,那么会进行身份验证,向主服务器发送一条AUTH命令
5.发送端口信息
从服务器向主服务器发送自己的监听端口号。
6.同步
从服务器向主服务器发送PSYNC命令,执行同步操作,并将自己的数据库更新至主服务器数据库当前的状态。执行完同步操作之后,主服务器也会成为从服务器的客户端。
7.命令传播
完成同步后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器。
在此阶段中,从服务器会默认以每秒一次的频率向主服务器发送命令:REPLCONF ACK <replication_offset(当前复制偏移量)>
此命令有三个作用:
1. 检测主从服务器的网络状态(心跳包)
如果主服务器超过一秒没有收到从服务器发来的REPLCONF ACK命令,那么主从服务器之间的连接出现问题了。
2. 辅助实现min-slave选项
通过min-slaves-to-write和min-slaves-max-lag两个选项可以防止主服务器在不安全的情况下执行写命令。
min-slaves-to-write x表示从服务器数量小于x时主服务器拒绝执行写命令
min-slaves-max-lag 10 表示x个从服务器的延迟都大于等于10秒时,主服务器将拒绝执行写命令。
3. 检测命令丢失
主服务器通过心跳包的replication_offset判断当前的从服务器是否落后于自己,然后主服务器就会根据偏移量在复制积压缓冲区里找到从服务器缺少的数据,并将这些数据重新发送给从服务器。
在我们之前介绍Redis的时候我们就提到说,Redis有两种持久化措施,AOF持久化和RDB持久化。其中AOF持久化就类似于Raft中的log落盘, RDB持久化就是 生成整个Redis数据库的快照文件。当一个新的节点加入成为slave节点或者当前的某个slave节点数据落后Master节点数据过多时,Master会生成RDB文件或者 把已有的AOF文件直接发给slave节点进行状态初始化。
我们同样以快照为例, 与Raft不同的是, 在Redis中,只要slave节点接收到了来自master节点的RDB文件, 就会把application state machine(即Redis 内存数据库)全部清空,并用RDB文件来初始化。
前面我们提到,Redis和Raft的master节点都在内存中维护一个buffer来缓存来自客户端的请求,同时在触发一些规则后把buffer的数据落盘。同时落盘的数据 就从缓存中删除掉。那么当follower节点/slave节点的数据虽然落后于master节点,但是缺少的部分还被master节点缓存在内存中的时候,就会触发部分复制, 即缺啥补啥。部分复制包含命令转发和补发过往数据。 在这部分Redis和Raft采用的策略是非常不一致的。Raft中数据是leader节点PUSH给follower节点的, 节点数据更新的进度是由Master节点维护的(即[]nextIndex数组), 即Master决定该同步给follower节点什么数据,而Redis中数据同步是通过slave节点从master节点PULL数据完成,同步进度(repl_offset)是由各个slave节点自己 维护的,即Slave节点决定需要同步哪些数据。接下来我们一一介绍下。
前文提到Redis采用的是ANY/ONE的做法, 当接收到来自客户端的数据更改请求时, 只要Master处理完该请求,就立马返回客户端。同时异步的把这些命令 转发给所有的slave节点。但是转发的过程中可能因为网络问题导致数据丢失。于是Redis的master节点在内存中开辟一个默认大小为1M的环形缓冲区。 在转发给slave节点用户请求的同时把该请求写入环形缓冲区。每个slave节点接收到一个命令之后会自增repl_offset值用来记录复制偏移量。 同时master节点也会记录当前缓冲区的最新命令的repl_offset, 与Raft中不同的是,这里的复制偏移量是字节偏移量,并不是命令的Index。salve节点会 定期的向master节点发送PSYNC命令请求同步复制,master节点会检查slave节点的复制偏移量是否在当前master节点的缓冲区中,如果在则从缓冲区中发送 命令,否则触发全量复制。事实上Redis只是采用了repl_offset有很大的问题,就是顺序很可能会出错。可能Master接收到的请求是PUT(KEY, 1), PUT(KEY, 2) ,但是salve接收到的顺序是PUT(KEY, 2), PUT(KEY, 1)。因为slave端没有办法去检查。而再Raft中是有一个递归性质来保证follower节点的log的顺序与leader 节点的log顺序是一致的。
13、Redis中的事务
实现步骤:
1、事务开始
命令MULTI命令将对应的redisClient从非事务状态切换至事务状态
通过设置REDIS_MULTI标识来表示
2、命令入队
每个redisClient都有自己的事务状态,保存在multiState中,事务状态内还包含着一个事务队列,以及一个已经入队命令的计数器。
typedef struct redisClient {
// 事务状态
multiState mstate;
} redisClient;
typedef struct multiState {
// 事务队列(数组),FIFO顺序
multiCmd *commands;
//已入命令计数
int count;
} multiState;
typedef struct multiCmd {
// 参数(二维数组)
robj **argv;
// 参数数量
int argc;
// 命令指针
struct redisCommand *cmd;
} multiCmd;
3、事务执行
当一个处于事务状态的客户端向服务器发送EXEC命令时,这个EXEC命令将立即被服务器执行。服务器会遍历这个客户端的事务队列,执行所有保存的命令,最后将执行命令所得的结果全部返回给客户端。
1、遍历事务队列中的每个项,读取命令参数,参数个数,执行命令。
2、执行每个命令,并将结果加入回复队列中
3、移除REDIS_MULTI标识,清零入队命令计数器,释放事务队列,让客户端回到非事务状态
4、返回执行结果给客户端
WATCH:redis中的乐观锁,可以监视任意数量的数据库键,在EXEC命令执行时,如果被监视的键修改过了,则服务器将拒绝执行事务。
WATCH实现: 每个redis数据库都会保存着一个watched_keys字典,字典的key是被监视的数据库键,值为监视客户端的链表。
监视机制的触发:所有对数据库进行修改的命令,在执行之后都会调用multi.c/touchWatchKey函数对watched_keys字典检查,如果有客户端正在监视此键,那么就会修改监视客户端的REDIS_DIRTY_CAS表示,表示该客户端的事务安全性已经被破坏。事务在执行的时候会判断客户端的REDIS_DIRTY_CAS是否打开,如果打开了那么服务器会拒绝执行客户端提交的事务。
redis事务的ACID性质:redis中事务总是具有原子性、一致性、隔离性,不一定具有持久性(运行在AOF持久化模式下才有)
原子性:用事务队列+单线程运行的方式保证,并且redis中不支持回滚操作
一致性:redis通过入队错误、执行错误、停机等错误的解决来保证数据库在执行事务前后的一致性
隔离性:redis使用单线程执行事件,因此隔离性也得到了保证
持久性:当服务器运行在AOF持久化下,并且appendfsync选项为always时,程序才会在执行命令后调用sync将数据真正保存到硬盘中,这种配置下事务是具有持久性的