redis学习 (1) 基础入门

目录

中文文档https://redis.com.cn/

官方文档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

    1. "k1"
    2. "v1"
    3. "k2"
    4. "v2"
    5. "k3"
    6. "v3"
    7. "k4"
    8. "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

    1. "!"
    2. "world"
    3. "hello"
  • 列表可以包含 2 32 − 1 2^{32}-1 232−1个元素(redis使用32位无符号整数存储列表长度)

Sets

  • 这是一个不重复元素的无序集合,增删查的时间复杂度都是O(1),存储的成员数量也是 2 32 − 1 2^{32}-1 232−1

    127.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

    1. "k1"
    2. "k2"
    3. "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

    1. "k5"
    2. "-1"
    3. "k1"
    4. "1"
    5. "k2"
    6. "1"
    7. "k3"
    8. "2"
    9. "k4"
    10. "3"
      127.0.0.1:6379> ZREVRANGE sortedsets 0 10 WITHSCORES
    11. "k4"
    12. "3"
    13. "k3"
    14. "2"
    15. "k2"
    16. "1"
    17. "k1"
    18. "1"
    19. "k5"
    20. "-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

    1. OK
    2. "redis"
    3. (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默认的持久化方式,通过在指定的时间间隔内生成数据集的时间点快照来备份数据

触发机制

  1. 自动触发

    • 满足配置的save条件
    • 执行FLUSHALL命令
    • 执行复制操作
  2. 手动触发

    • 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. 文件大小适中

对比

  1. RDB:二进制格式,高度压缩,只存储最终的数据状态
  2. AOF:文本格式,存储所有的写操作命令,包括中间过程
  3. AOF文件比RDB更新频率高,优先使用AOF还原数据。
  4. AOF比RDB更安全,也更大
  5. RDB性能比AOF好
  6. 如果两个都配了优先加载AOF
相关推荐
qq_571099351 小时前
学习周报二十五
学习
崇山峻岭之间1 小时前
C++ Prime Plus 学习笔记027
c++·笔记·学习
天生励志1231 小时前
Redis 安装部署
数据库·redis·缓存
北半球的夜1 小时前
emoji 表情符号保存问题
数据库·oracle
Xudde.2 小时前
BabyPass靶机渗透
笔记·学习·安全·web安全
清风6666662 小时前
基于单片机的智能家居多参数环境监测与联动报警系统设计
数据库·单片机·毕业设计·智能家居·课程设计·期末大作业
kblj55552 小时前
学习Linux——学习工具——DNS--BIND工具
linux·运维·学习
煎蛋学姐2 小时前
SSM社区医院儿童预防接种管理系统84ui9(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·ssm 框架·儿童预防接种
立志成为大牛的小牛2 小时前
数据结构——五十四、处理冲突的方法——开放定址法(王道408)
数据结构·学习·程序人生·考研·算法