37

Redis从生米煮成熟饭

 4 years ago
source link: http://www.qianshan.tech/%E6%9E%B6%E6%9E%84/%E9%AB%98%E5%B9%B6%E5%8F%91/Redis%E4%BB%8E%E7%94%9F%E7%B1%B3%E7%85%AE%E6%88%90%E7%86%9F%E9%A5%AD.html?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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(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方式配置

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中文官方网站

be1412c6890280b7976893810e491212df3.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK