目录
官方文档https://redis.io/docs/latest/develop/
概念
- r e d i s ( R e m o t e D i c t i o n a r y S e r v e r ) redis(Remote\ Dictionary\ Server) redis(Remote Dictionary Server)是一个开源的高性能kv存储系统,它是一个基于内存的数据结构存储系统,可以用作数据库、缓存和消息代理
安装
- 我们可以使用
docker简单的安装下单机的redis实例。redis实例的其他模式,在第2节介绍
参考https://www.runoob.com/docker/docker-install-redis.html
数据类型
- redis启动之后,我们使用
redis-cli就可以进入到redis命令行模式

PING返回PONG,说明redis实例正常 - redis存储的是kv的数据,所以每个数据都是一个键值对,键的类型是字符串
Strings
-
可以用简单的
SET key value存储一个kv对,可以设置一些过期时间等等参数127.0.0.1:6379> set mykey "Hello" EX 5
OK
127.0.0.1:6379> get mykey
"Hello" -
这样就存储了一个字符串类型的数据
其他命令,参考https://redis.com.cn/redis-strings.html
Hashes
-
哈希类型适合存储对象,它是一个string类型的field和value的映射表
127.0.0.1:6379> HMSET mykey k1 v1 k2 v2 k3 v3 k4 v4
OK
127.0.0.1:6379> HGETALL mykey- "k1"
- "v1"
- "k2"
- "v2"
- "k3"
- "v3"
- "k4"
- "v4"
-
上面的例子,数据的key是
mykey,包含详细信息k1,k2,k3,k4
Lists
-
这个数据结构类似于链表,下面展示了如何把一些数据放到redis的列表中,并把它们拿出来
127.0.0.1:6379> LPUSH list hello
(integer) 1
127.0.0.1:6379> LPUSH list world
(integer) 2
127.0.0.1:6379> LPUSH list !
(integer) 3
127.0.0.1:6379> LRANGE list 0 4- "!"
- "world"
- "hello"
-
列表可以包含 2 32 − 1 2^{32}-1 232−1个元素(redis使用32位无符号整数存储列表长度)
Sets
-
这是一个不重复元素的无序集合,增删查的时间复杂度都是
O(1),存储的成员数量也是 2 32 − 1 2^{32}-1 232−1127.0.0.1:6379> SADD set k1
(integer) 0
127.0.0.1:6379> SADD set k2
(integer) 0
127.0.0.1:6379> SADD set k3
(integer) 0
127.0.0.1:6379> SADD set k1
(integer) 0
127.0.0.1:6379> SMEMBERS set- "k1"
- "k2"
- "k3"
Sorted Sets
-
这种数据结构和set的区别是,每个元素会关联一个double类型的分数,通过分数给元素排序
127.0.0.1:6379> ZADD sortedsets 1 k1
(integer) 1
127.0.0.1:6379> ZADD sortedsets 1 k2
(integer) 1
127.0.0.1:6379> ZADD sortedsets 2 k3
(integer) 1
127.0.0.1:6379> ZADD sortedsets 3 k4
(integer) 1
127.0.0.1:6379> ZADD sortedsets -1 k5
(integer) 1
127.0.0.1:6379> ZRANGE sortedsets 0 10 WITHSCORES- "k5"
- "-1"
- "k1"
- "1"
- "k2"
- "1"
- "k3"
- "2"
- "k4"
- "3"
127.0.0.1:6379> ZREVRANGE sortedsets 0 10 WITHSCORES - "k4"
- "3"
- "k3"
- "2"
- "k2"
- "1"
- "k1"
- "1"
- "k5"
- "-1"
ZRANGE返回分数从低到高,ZREVRANGE返回从高到低
事务
-
下面是一个使用事务执行多条命令的例子
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> EXEC
(empty list or set)
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET rediscomcn redis
QUEUED
redis 127.0.0.1:6379> GET rediscomcn
QUEUED
redis 127.0.0.1:6379> INCR visitors
QUEUED
redis 127.0.0.1:6379> EXEC- OK
- "redis"
- (integer) 1
-
redis的事务和mysql的事务有本质上的区别。redis的事务仅仅是把多条命令打包到一块批处理,减少网络交互次数。是一种尽力而为的原子性,当redis服务端收到EXEC的时候,开始执行事务,中间的命令执行失败,不会导致前面的命令回滚,也不会造成后面的命令不做。也就是说,无法保证ACID
-
可以使用WATCH来监控某个key,如果key被修改了,无法触发后面的事务。像下面这个例子
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> watch age //开始监视age
OK
127.0.0.1:6379> set age 24 //在EXEC之前,age的值被修改了
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 25
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> exec //触发EXEC
(nil) //事务无法被执行
持久化
- redis作为内存数据库,数据备份和持久化是保证数据安全的关键技术,redis主要提供了两种主要的持久化方式:
RDB(Redis Database Backup)快照和AOF(Append Only File)日志,以及Redis 4.0+引入的混合持久化
RDB
- RDB是redis默认的持久化方式,通过在指定的时间间隔内生成数据集的时间点快照来备份数据
触发机制
-
自动触发
- 满足配置的save条件
- 执行FLUSHALL命令
- 执行复制操作
-
手动触发
- SAVE命令(同步)
- BGSAVE命令(异步)
源码分析
c
// rdb.c - RDB保存核心函数
int rdbSave(char *filename) {
FILE *fp;
rio rdb;
int error = 0;
// 创建临时文件
snprintf(tmpfile,256,"temp-%d.rdb", (int) getpid());
fp = fopen(tmpfile,"w");
if (!fp) {
serverLog(LL_WARNING, "Failed opening .rdb for saving: %s",
strerror(errno));
return C_ERR;
}
// 初始化RDB I/O
rioInitWithFile(&rdb,fp);
// 写入RDB文件头
if (rdbWriteRaw(&rdb,magic,9) == -1) goto werr;
if (rdbSaveInfoAuxFields(&rdb,flags,rsi) == -1) goto werr;
// 遍历所有数据库
for (j = 0; j < server.dbnum; j++) {
redisDb *db = server.db+j;
dict *d = db->dict;
if (dictSize(d) == 0) continue;
// 写入数据库选择器
if (rdbSaveType(&rdb,RDB_OPCODE_SELECTDB) == -1) goto werr;
if (rdbSaveLen(&rdb,j) == -1) goto werr;
// 写入键值对
di = dictGetSafeIterator(d);
while((de = dictNext(di)) != NULL) {
sds keystr = dictGetKey(de);
robj key, *o = dictGetVal(de);
long long expire;
initStaticStringObject(key,keystr);
expire = getExpire(db,&key);
// 保存键值对
if (rdbSaveKeyValuePair(&rdb,&key,o,expire) == -1) goto werr;
}
dictReleaseIterator(di);
}
// 写入EOF标记
if (rdbSaveType(&rdb,RDB_OPCODE_EOF) == -1) goto werr;
// 写入校验和
cksum = rdb.cksum;
memrev64ifbe(&cksum);
if (rioWrite(&rdb,&cksum,8) == 0) goto werr;
// 刷新并关闭文件
if (fflush(fp) == EOF) goto werr;
if (fsync(fileno(fp)) == -1) goto werr;
if (fclose(fp) == EOF) goto werr;
// 原子性重命名
if (rename(tmpfile,filename) == -1) {
serverLog(LL_WARNING,"Error moving temp DB file on the final destination: %s", strerror(errno));
unlink(tmpfile);
return C_ERR;
}
return C_OK;
werr:
serverLog(LL_WARNING,"Write error saving DB on disk: %s", strerror(errno));
fclose(fp);
unlink(tmpfile);
return C_ERR;
}
// BGSAVE异步保存
int rdbSaveBackground(char *filename, rdbSaveInfo *rsi) {
pid_t childpid;
if (server.rdb_child_pid != -1) return C_ERR;
server.dirty_before_bgsave = server.dirty;
server.lastbgsave_try = time(NULL);
// fork子进程
if ((childpid = fork()) == 0) {
// 子进程执行保存
int retval;
closeListeningSockets(0);
redisSetProcTitle("redis-rdb-bgsave");
retval = rdbSave(filename);
if (retval == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"RDB: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
}
exitFromChild((retval == C_OK) ? 0 : 1);
} else {
// 父进程
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024);
if (childpid == -1) {
serverLog(LL_WARNING,"Can't save in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,"Background saving started by pid %d",childpid);
server.rdb_save_time_start = time(NULL);
server.rdb_child_pid = childpid;
server.rdb_child_type = RDB_CHILD_TYPE_DISK;
return C_OK;
}
return C_OK; /* unreached */
}
RDB文件格式
RDB文件结构:
+-------+-------------+-----------+-----------------+-----+-----------+
| REDIS | RDB-VERSION | SELECT-DB | KEY-VALUE-PAIRS | EOF | CHECK-SUM |
+-------+-------------+-----------+-----------------+-----+-----------+
详细格式:
- REDIS: 5字节魔数 "REDIS"
- RDB-VERSION: 4字节版本号
- SELECT-DB: 数据库选择标记
- KEY-VALUE-PAIRS: 键值对数据
- EXPIRE-TIME: 过期时间(可选)
- VALUE-TYPE: 值类型
- KEY: 键名
- VALUE: 值内容
- EOF: 结束标记
- CHECK-SUM: 8字节CRC64校验和
RDB配置优化
bash
# redis.conf - RDB配置
# 自动保存条件
save 900 1 # 900秒内至少1个key变化
save 300 10 # 300秒内至少10个key变化
save 60 10000 # 60秒内至少10000个key变化
# RDB文件配置
dbfilename dump.rdb
dir /var/lib/redis/
# 压缩配置
rdbcompression yes
rdbchecksum yes
# 错误处理
stop-writes-on-bgsave-error yes
# 无盘复制(适用于磁盘较慢的环境)
repl-diskless-sync no
repl-diskless-sync-delay 5
AOF
- AOF通过记录每个写操作命令来实现数据持久化,Redis重启的时候通过重新执行这些命令来恢复数据
c
// aof.c - AOF重写核心函数
int rewriteAppendOnlyFileBackground(void) {
pid_t childpid;
long long start;
if (server.aof_child_pid != -1 || server.rdb_child_pid != -1) return C_ERR;
if (aofCreatePipes() != C_OK) return C_ERR;
start = ustime();
if ((childpid = fork()) == 0) {
// 子进程执行AOF重写
char tmpfile[256];
closeListeningSockets(0);
redisSetProcTitle("redis-aof-rewrite");
snprintf(tmpfile,256,"temp-rewriteaof-bg-%d.aof", (int) getpid());
if (rewriteAppendOnlyFile(tmpfile) == C_OK) {
size_t private_dirty = zmalloc_get_private_dirty(-1);
if (private_dirty) {
serverLog(LL_NOTICE,
"AOF rewrite: %zu MB of memory used by copy-on-write",
private_dirty/(1024*1024));
}
server.child_info_data.cow_size = private_dirty;
sendChildInfo(CHILD_INFO_TYPE_AOF);
exitFromChild(0);
} else {
exitFromChild(1);
}
} else {
// 父进程
server.stat_fork_time = ustime()-start;
server.stat_fork_rate = (double) zmalloc_used_memory() * 1000000 / server.stat_fork_time / (1024*1024*1024);
if (childpid == -1) {
close(server.aof_pipe_write_data_to_child);
close(server.aof_pipe_read_ack_from_child);
close(server.aof_pipe_write_ack_to_parent);
close(server.aof_pipe_read_data_from_parent);
serverLog(LL_WARNING,
"Can't rewrite append only file in background: fork: %s",
strerror(errno));
return C_ERR;
}
serverLog(LL_NOTICE,
"Background append only file rewriting started by pid %d",childpid);
server.aof_rewrite_scheduled = 0;
server.aof_rewrite_time_start = time(NULL);
server.aof_child_pid = childpid;
return C_OK;
}
return C_OK;
}
// AOF写入函数
void feedAppendOnlyFile(struct redisCommand *cmd, int dictid, robj **argv, int argc) {
sds buf = sdsempty();
robj *tmpargv[3];
// 如果需要,先写入SELECT命令
if (dictid != server.aof_selected_db) {
char seldb[64];
snprintf(seldb,sizeof(seldb),"%d",dictid);
buf = sdscatprintf(buf,"*2\r\n$6\r\nSELECT\r\n$%lu\r\n%s\r\n",
(unsigned long)strlen(seldb),seldb);
server.aof_selected_db = dictid;
}
// 写入命令
if (cmd->proc == expireCommand || cmd->proc == pexpireCommand ||
cmd->proc == expireatCommand) {
// 转换相对过期时间为绝对过期时间
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
} else if (cmd->proc == setexCommand || cmd->proc == psetexCommand) {
// 转换SETEX为SET + EXPIREAT
tmpargv[0] = createStringObject("SET",3);
tmpargv[1] = argv[1];
tmpargv[2] = argv[3];
buf = catAppendOnlyGenericCommand(buf,3,tmpargv);
decrRefCount(tmpargv[0]);
buf = catAppendOnlyExpireAtCommand(buf,cmd,argv[1],argv[2]);
} else {
// 普通命令
buf = catAppendOnlyGenericCommand(buf,argc,argv);
}
// 写入AOF缓冲区
if (server.aof_state == AOF_ON)
server.aof_buf = sdscatlen(server.aof_buf,buf,sdslen(buf));
sdsfree(buf);
}
AOF配置详解
bash
# redis.conf - AOF配置
# 启用AOF
appendonly yes
appendfilename "appendonly.aof"
# 同步策略
appendfsync everysec # 推荐设置
# appendfsync always # 最安全但性能最差
# appendfsync no # 最快但可能丢失数据
# AOF重写配置
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# AOF加载配置
aof-load-truncated yes
aof-use-rdb-preamble yes # 混合持久化
混合持久化
工作原理
Redis 4.0引入的混合持久化结合了RDB和AOF的优点:
- AOF重写时,将RDB格式的数据写入AOF文件开头
- 后续的增量数据以AOF格式追加
混合持久化文件格式
混合持久化AOF文件结构:
+-------+-------------+-----------+-----------------+-----+-------------+
| REDIS | RDB-VERSION | RDB-DATA | AOF-COMMANDS | ... | MORE-CMDS |
+-------+-------------+-----------+-----------------+-----+-------------+
|<------------- RDB格式前导 ----------->|<------- AOF格式增量 ------>|
优势:
1. 恢复速度快(RDB部分)
2. 数据安全性高(AOF部分)
3. 文件大小适中
对比
- RDB:二进制格式,高度压缩,只存储最终的数据状态
- AOF:文本格式,存储所有的写操作命令,包括中间过程
- AOF文件比RDB更新频率高,优先使用AOF还原数据。
- AOF比RDB更安全,也更大
- RDB性能比AOF好
- 如果两个都配了优先加载AOF