Redis从生米煮成熟饭

Redis为何选用单线程

单线程减少线程上下文切换和锁竞争。

网络IO模型采用IO多路复用,使用EPOLL注册读写事件通知,同步非阻塞。

Redis单线程如何发挥多核CPU优势

在单台服务器上运行多个redis实例。 使用taskset命令,将每个redis实例和cpu核心进行绑定

Redis分布式锁实现

setnx key value 如果key不存在,则创建并赋值。成功返回1 失败返回0。 同时设置过期时间: expire key seconds

也可使用set命令配合NX EX选项一次性设置 SET key value EX 10086 NX

如何安全地释放锁

释放锁,即是删除key。但需要注意避免锁误删(删除其他线程加的锁)的问题。

场景: 假如A线程成功获取锁,但执行时间过长导致锁释放。这时B线程获取了锁开始执行。那么A执行完成后删除的可能是B加的锁。

简单处理可以在加锁时将value设置为自己线程ID或者当前线程生成的随机数,删除之前先判断锁对应的值是否与加锁时一致,而由此衍生的问题是判断锁的键值和删除锁是两个操作,如何保证原子性?

Redis官方提出了用LUA脚本将查询比较删除组合成一组命令来保证原子性的方案

 if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end

话说Redis内置了对lua语言的支持,其实lua最终还是依次执行查询删除的命令,但是Redis是单线程处理,lua将两个命令当成一组,处理LUA请求时就屏蔽了其他的请求。间接实现了原子性。

Redisson

  1. 如果锁过期了业务逻辑还没执行完怎么办
  2. 如果redis实例单点故障了怎么办
  3. 分布式锁如何实现可重入

Redisson封装了包括上述针对上述问题在内的很多解决方案: 官方文档

Redisson分布式锁原理图

Redisson分布式锁原理图

  1. 锁续期原理: Redisson在加分布式锁时会开启watchDog,每隔一定时间轮询,对锁进行续期。
  2. 可重入原理: Redisson的锁为Hash对象,Hash对象中的属性key为请求加锁的线程id,值为锁重入的次数。加锁时对比线程id是否和锁对象中的值一致。 分析加锁LUA脚本(KEY[1]:锁id, ARGV[1]:失效时间, ARGV[2]:请求加锁的线程id)
    -- 如果锁id不存在直接加锁成功返回
    if (redis.call('exists', KEYS[1]) == 0) then 
    redis.call('hset', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
    end; 
    --如果锁id存在,比较锁中的对象值,判断持有锁的是否为当前线程,是则累加锁重入次数
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
    redis.call('hincrby', KEYS[1], ARGV[2], 1); 
    redis.call('pexpire', KEYS[1], ARGV[1]); 
    return nil; 
    end; 
    return redis.call('pttl', KEYS[1]);"
    
  3. 针对锁的可用问题,提出RedLock的解决方案,在多节点实例中上锁,只要大多数节点上锁成功就视为成功。

缓存雪崩、缓存击穿、缓存穿透

缓存雪崩: 大量的KEY过期时间过于集中,导致瞬时很多缓存失效,由此可能导致数据库压力陡然升高。
解决方案: 将失效时间随机打乱,如在系统启动预热时设定一定程度上离散的过期时间。

缓存击穿: 缓存中某一个KEY过期失效,如果此时有大量请求过来无法命中缓存的KEY,缓存层像被凿开了一个口子一样流入大量数据库查询请求。也算是一种惊群效应。
解决方案:双重校验方式从数据库中读取数据到缓存。双重校验:第一层查询缓存失败后,进入临界区,保证同时只有一个请求线程读取数据库,进入临界区后再次尝试缓存,仍然没有命中则查询数据库。

缓存穿透: 外部请求不断查询一个系统中不存在的数据,服务无法命中缓存转而每次尝试从数据库中查询。
解决方案

  1. 对查询结果为空key设置值为null的缓存,牺牲缓存空间换响应时间。
  2. 把所有非法的key映射到一个bitmap中,通过bitmap拦截。《布隆过滤器》原理

Redis如何删除过期key

  1. 主动删除:redis 默认每隔一定时间检查已过期key进行删除, 或者内存不足时触发主动删除机制

  2. 惰性删除:在有请求读写key时再检查key是否过期,过期则删除

Redis如何持久化

AOF

AOF(Append Only File ): 记录每次redis的写命令,如对一个key更新10次,记录10条写指令。可以设置每秒一次或者每个写动作发生后追加。 优点: 持久化频率高,异常down机时数据丢失少。 缺点: 文件大,恢复时相对耗时。

Redis默认使用RDB持久化,可同时开启AOF持久化,如下

appendonly yes
appendfilename "appendonly.aof"

AOF重写

AOF文件体积过大时会进行重写。重写的作用是用一条命令去记录键值对,代替之前记录该键值对的多个命令。如一个key自增100次,原文件记录了100条指令,重写后将原来的指令合并成为最新的一条,由此大大压缩AOF文件体积。

重写采用写时赋值(Copy On Write)原理,不直接操作原文件。

RDB

RDB:快照持久化。在特点时间点保存全量的数据信息。 优点: 恢复时直接将快照文件加载到内存,速度快。 缺点: 因为全量数据量大,持久化频率一般设置较低。异常关机时会丢失上次持久化到关机时刻的变更数据。

如RDB方式配置

save 900 1 #在900s内如果有1条数据被写入,则产生一次快照。 
save 300 10#在300s内如果有10条数据被写入,则产生一次快照   
save 60 10000 #在60s内如果有10000条数据被写入,则产生一次快照  
stop-writes-on-bgsave-error yes    # 如果为yes则表示,当备份进程出错的时候,   主进程就停止进行接受新的写入操作,这样是为了保护持久化的数据一致性的问题。

bgsave

也可通过basave命令手动触发快照的持久化。bgsave fork出子进程,采用写时复制机制写入。不阻塞主进程的工作(对比save命令)。

AOF和RDB可搭配同时使用,开启AOF时,redis启动默认从AOF日志文件中重建缓存。

Redis集群方案

  1. Sentinel: 哨兵模式,主要实现高可用。解决Redis原有的主从同步模式,当Master实例故障时导致整个集群无法工作的缺陷。Sentinel是一个独立运行的进程,检测Redis集群状态,如Master因故障下线时则在集群机器中发起选举选出新的Master节点。
  2. Cluster: 解决单台主机的内存容量有限。 Redis集群定义有16384个哈希槽,每个节点负责一段。每个key通过CRC16校验后对16384取模来决定放置哪个分片。

Redis和数据库不一致问题

数据库更新时需要触发缓存更新

如果先删除缓存再进行数据库更新, 可能在数据库更新之前,新的请求进来因为没有命中缓存从数据库中读取旧数据。 如果先进行数据库更新在删除缓存,可能出现数据库写入成功后删除缓存失败。导致缓存仍然是旧数据。

网上流传双删策略: 先删除缓存–> 数据库写入 ——》再次删除缓存。

好处是能数据更新前能预检查下redis工作状态,但是关键的问题:写入后删除缓存时失败的情况仍然可能存在。根本的解决方案应该在写入后删除失败时增加重试机制,或者回滚数据库更新。

Redis内存淘汰策略

Redis使用内存超过 maxmemory 配置大小时触发内存淘汰策略。

  • noeviction:不删除策略,内存达到上限时直接返回错误信息
  • allkeys-random: 针对所有的key,随机删除一部分
  • allkeys-lru: 针对所有的key,优先删除最少使用的
  • volatile-random: 针对设置了过期时间的key,随机删除一部分
  • volatile-ttl: 针对设置了过期时间的key,优先删除最快过期的key

Redis热点Key优化

如微博热门话题导致某个key被大量请求命中成为热点key,流量过于集中导致key所在机器的网卡带宽被大量占用,key如果突然过期也会出现上述的“缓存击穿”风险。

检测热点key

  1. 通过monitor命令获取redis实时命令,分析结果统计出热点key(monitor只能统计单个redis节点):
    redis-cli monitor | head -n 5000
    
  2. redis4.0以上版本,直接使用–hotkeys选项
    redis-cli --hotkeys
    

优化处理

  1. 通过服务程序的本地缓存,避免将所有的请求流量压力都转发到redis, 如encache+redis二级缓存方案。
  2. 通过proxy访问模式,识别热点key之后,为热点key在其他机器中增加副本,proxy层将请求流量分发到多个机器。

Redis大Key优化

检测大key

redis-cli --bigkeys 

优化存储

避免单个key空间太大,添加前缀拆分为多个KEY,根据元素的Hash结果决定该值落在哪个key上。

优化删除

大key的删除容易导致redis长时间阻塞。
常用渐进式删除,先对需删除的key重命名,使对应key消失在redis命名空间,再异步逐步删除key的内存元素。
redis4以上版本,可以直接使用unlink 命令直接实现渐进式方式的删除。

参考

Redis中文官方网站