redis基础

322 阅读6分钟

redis持久化数据两种方式RDB和AOF

RDB

储存:

save命令(会阻塞服务,期间redis不能处理请求)
一般用BGSAVE命令(会派生一个子进程进行操作,不阻塞服务)
配置文件conf中开启 900 1或 300 10 或 60 10000 ,
配置分别表示15分钟内至少对数据修改一次 或5分钟300次 或1分钟10000次, 就会自动进行一次内存快照(转为一个压缩的二进制数据文件也就是.rdb文件)

载入:

只有AOF是关闭才会找.rdb文件进行数据还原,否则会找AOF文件还原数据。 redis在载入rdb文件 的时候会一直阻塞服务,也就是redis无法处理请求知道载入成功

AOF(推荐)

储存:

生成appendonly.aof文件,里面记录了操作redis的命令,还原的时候redis也会读取命令进行数据还原(类似于MySQL的binlog)
配置文件conf开启后选项:
1、always(最安全但是效率最慢,服务器在每个事件循环都要将所有内容调用系统的write写入aof_buf缓冲区中,并且用子线程立即调用操作系统fsync函数强制同步到AOF文件)
2、everysec(每秒进行一次同步,事故宕机最多丢失一秒的命令数据, 服务器在每个事件循环都要将所有内容调用系统的write写入aof_buf缓冲区,用子线程每秒调用系统fsync函数强制同步一次)
3、no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证

注意:对于操作系统,用户嗲用write海曙将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区 中,当缓冲区满载或者超过知道时限后才真正将数据写入硬盘,这样提高了效率也带了安全问题, 因此操作系统提供了fsync 、fdatasync等强制操作系统同步硬盘文件的函数,保证数据安全。

AOF文件的重写:

手动触发:直接调用bgrewriteaof命令,该命令的执行与bgsave有些类似:都是fork子进程进行具体的工作,且都只有在fork时阻塞。
自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数,以及aof_current_size和aof_base_size状态确定触发时机。 auto-aof-rewrite-min-size: 执行重写时aof文件的最小体积默认是64Mb auto-aof-rewrite-percentage: 执行重写时aof文件和上一次重写前aof文件大小的比值 重写步骤:
1、 先判断是否有正在bgrewriteaof的子进程,有直接返回 不在重写,如果有bgsave的子进程在执行则等bgsave执行完成后再执行bgrewriteaof
2、主进程执行fork创建子进程,这个过程会阻塞。 创建完成后redis正常接受请求
3、fork的子进程只能拿到fork线程的时候的redis内存数据快照。之后redis处理的请求会将数据写入两份,一份写入aof_buf缓冲区然后按规则调用fsync强制同步到aof文件另一份会写入aof_rewirte_buf缓冲区
4、子进程根据内存快照,按命令合并规则写入新的AOF文件,完成后向主线程发信号, 主线程将aof_rewirte_buf缓冲区的数据写入到新的AOF文件, 使用新的AOF文件替换老文件,完成AOF重写

载入:

载入AOF文件时命令是直接从文件中读取的,并不是由客户端发送;因此Redis服务器在载入AOF文件之前,会创建一个没有网络连接的伪客户端,之后用它来执行AOF文件中的命令,命令执行的效果与带网络连接的客户端完全一样。

常见问题

1、redis缓存穿透:key对应的数据不存在,每次请求走的都是databases,大量的并发这些key的话,会请求数据源,会压垮数据库
2、redis缓存击穿:key对应的数据已经过期,同一时间大量的并发这个key的话,会请求数据源,压垮数据库(key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题。)
3、缓存雪崩: redis服务重启的时候或者大量的key同一时间过期

解决方案

1、redis缓存穿透:最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的map中,一个一定不存在的数据会被 这map拦截掉。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟
2、redis缓存击穿:
使用互斥锁解决(redis自带命令setnx, 表示set if not exists,不存在才赋值),这个命令带返回值表示赋值是否成功。我们在获取key缓存的时候,假设 key过期了,此时尝试设置一个任意不存在的key_mutex,如果成功则取数据库读取key的值在重新缓存key到redis,如果我们尝试设置key_mutex失败, 则代表此时有其他线程设置key_mutex成功了正在数据库读取key值,当前线程只需稍微等待一下重新获取key就行了。\

//伪代码:
public String get(key) {
     String value = redis.get(key);
     if (value == null) { // 缓存过期redis返回null
 	// key_mutex三分钟过期,防止del(key_mutex)失败的话下次key过期无法读取db
       if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { // ==1代表设置成功
            value = db.get(key);
            redis.set(key, value, expire_secs);
            redis.del((key_mutex,)
       }else {
           Thread.sleep(50) // 代表其他线程正在执行上面的代码,访问db,并缓存key到redis
            return get(key); // 重新获取key
       }
     }
     return value
}

3、redis缓存雪崩: 大量的key同时失效,我们需要避免这种情况(热点key), 可以设置一个key的标记key_sign的过期时间, 他是真正的key的过期时间的一半, 假设 key_sign 过期了则后台程序自动读取db 中key的数据, 更新key的值和过期时间。用户访问的其实还是redis.key

// 伪代码
public object GetProductListNew() {
    int expireTime = 30;
    String key = "product_list";
    //key的标记 key_sign
    String keySign = key + "_sign";
    
     //获取缓存值
    String value = CacheHelper.Get(key);
    String signValue = CacheHelper.Get(keySign);
    
    if (signValue != null) {
        return value; // key的标记key_sign还未过期,直接返回key的值
    } else { // key的标记key_sign还过期了, 此时用户需要的key并没有过期
        CacheHelper.Add(keySign, "1", expireTime);
        ThreadPool.QueueUserWorkItem((arg) -> {
             //这里一般是 sql查询数据,更新用户需要的key
            cacheValue = GetProductListFromDB(); 
            // 日期设缓存时间的2倍,用于脏读
          CacheHelper.Add(key, cacheValue, expireTime * 2);   
        });
        return value;
    }
}