Freeman's Blog

一个菜鸡心血来潮搭建的个人博客

0%

Redis

TODOS

  • [] Redis分布式锁

5大数据类型

String

  • 分布式锁
    • set if not exists: setnx key value
    • set key value [EX seconds] [PX milliseconds] [NX|XX]
      • ex: 几秒后过期
      • px:几毫秒后过期
      • NX:不存在的时候创建key
      • XX:存在的时候覆盖key
  • 需要计数的场景
  • 商品编号、订单号,使用INCR生成
  • 赞/踩

    List

  • 有序有重复/可以左右加入
  • lpush/rpush
  • brpop: 阻塞的pop操作
  • lrange: 分页查询
  • (离线的)消息推送。
  • 消息队列、慢查询

    Hash

  • hset/hmset
  • hget/hmget
  • 对象存储
  • 也可以实现分布式锁 -> 可重入锁
    • 记录过期时间和释放次数
  • 购物车?

    set

  • smembers
  • 无序无重复
  • 删除/加入/判重
  • 随机弹x个(删/不删)
  • 集合运算:差/交/并
  • 场景
    • 数据不能重复,需要进行集合操作
    • 抽奖:参与sadd,显示参与scard,抽取srandmember/spop
    • 点赞
    • 社交关系
    • 可能认识的人

      zset(Sorted set)

  • 有序无重复
  • 需要根据权重进行排序的情况。
  • 在线用户列表、排行榜、弹幕…

    bitmap

  • 用一个bit位来表示某个元素对应的值或者状态。key就是对应元素本身。-> 节省存储空间
  • 用户签到:offset = day % 365,key = 年份#用户ID
  • 统计活跃用户?BITOP [AND | OR | NOT | XOR]
  • 用户在线状态:用户ID为offset

    hyperloglogs

    geospatial

    stream

Redis底层数据结构

Redis为什么单线程,又为什么开始使用多线程

单线程模型

  • IO多路复用 + Reactor模式
    • 单线程带来更好的可维护性,方便开发调试
    • 单线程也能够并发。
    • 绝大多数操作的性能瓶颈不是CPU
      • 简单总结一下,Redis 并不是 CPU 密集型的服务,如果不开启 AOF 备份,所有 Redis 的操作都会在内存中完成不会涉及任何的 I/O 操作,这些数据的读写由于只发生在内存中,所以处理速度是非常快的;整个服务的瓶颈在于网络传输带来的延迟和等待客户端的数据传输,也就是网络 I/O,所以使用多线程模型处理全部的外部请求可能不是一个好的方案。
    • 减少线程创建和切换的开销
    • 不需要同步机制,可以保障单次操作的原子性

为什么引入多线程模型

  • 如果某个任务特别大,单次任务可能要消耗较多的时间,这些操作会阻塞待处理的任务。
  • 删除超大键值对:UNLINK,将key从元数据中删除,开启新线程在后台执行释放内存空间的工作。
  • 接受数据包并解析Redis命令,发送返回数据包:这些过程可以引入多线程进行并发处理。

Redis如何判断数据过期、删除策略、内存淘汰机制

Redis判断数据过期

过期字典:key就是数据库中的key,value是时间戳

Redis过期删除策略

  • 惰性删除:只有在取出key的时候对数据进行过期检查。对CPU友好,但可能会造成过期key太多。
  • 定期删除:隔一段时间抽取一批key执行删除过期key操作。执行时长和执行频率会对CPU时间造成影响。
  • 定期删除+惰性删除:每隔一段时间进行检查,但是控制请求的时间和范围,只对key进行随机抽检。然后在取出某个key的时候进行检查,如果过期则删除。
    • 即使使用这种策略,如果定期删除没有删除完全/没有对key进行请求/没有设置过期时间,redis的内存占用会越来越高,此时需要采用内存淘汰机制。

Redis内存淘汰机制

  • redis.conf中:maxmemory-policy [policy name]
  • volatile-lru:从设置了过期时间的数据集里进行LRU,只有在将redis既作为持久存储又作为缓存的时候才使用。
  • volatile-ttl:从设置了过期时间的数据级中选择即将过期的数据进行淘汰
  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  • allkeys-lru: 当内存不足以容纳新数据时对键空间中所有的key进行lru
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction(默认策略):禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。
  • volatile-lfu(least frequently used):从已设置过期时间的数据集中挑选最不经常使用的数据淘汰
  • allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

Redis持久化

  • 1.重启机器后重用数据、2. 防止系统故障将数据备份

    快照持久化(Snapshotting,RDB,默认持久化方式)

    创建快照,建立内存数据在某个时间点上的副本。可以对快照进行备份,可以将快照复制到其它服务器上(主从复制、读写分离)

    RDB的触发机制

  • save命令,是同步命令,会占用Redis主进程
  • bgsave命令,执行一个异步操作,使用fork()生成子进程将数据保存到硬盘上。主进程调用fork()时也会阻塞,但是比让主进程亲自创建RDB快多了。缺点在于消耗内存。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # Redis.conf
    save 900 100 # 在900秒后,如果至少有100个key发生变化,Redis就自动触发BGSAVE命令创建快照。

    # RDB持久化文件名
    dbfilename dump-<port>.rdb

    # 数据持久化文件存储目录
    dir /var/lib/redis

    # bgsave发生错误时是否停止写入,通常为yes
    stop-writes-on-bgsave-error yes

    # rdb文件是否使用压缩格式
    rdbcompression yes

    # 是否对rdb文件进行校验和检验,通常为yes
    rdbchecksum yes

    创建RDB的机制:写时复制

    当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作:
  1. Redis 调用forks。同时拥有父进程和子进程。
  2. 子进程将数据集写入到一个临时 RDB 文件中。
  3. 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

RDB的优缺点

  • 优点
    • 紧凑、适合数据集的备份、用RDB进行备份恢复比AOF快、可以隔段时间进行备份,创建符合不同要求的数据还原点。
    • 方便传送到远端机器,适合容灾恢复。
    • 使用BGSAVE的方式,Redis主进程除了fork不需要任何其他操作。
  • 缺点
    • 频繁进行fork操作也会影响性能。
    • 不可控,存在数据丢失风险。将数据集进行全量备份是耗时的工作,如果Redis宕机,只能保存上一个检查点记录的数据。

AOF持久化(Append-Only File)

比起RDB持久化,AOF持久化的实时性更好。

1
2
# Redis.conf
appendonly yes

每执行一条更改Redis中数据的命令,Redis就将命令写入硬盘中的AOF文件,保存位置和RDB文件的位置相同。

AOF持久化的不同方式

通过配置文件设置Redis隔多长时间fsync到磁盘一次。

1
2
3
4
# Redis.conf
appendfsync always #每次有数据修改发生时都会写入AOF文件,好处是不丢数据,但IO开销大,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘,但可能丢失1秒数据
appendfsync no #让操作系统决定何时进行同步

AOF重写

因为 AOF 的运作方式是不断地将命令追加到文件的末尾, 所以随着写入命令的不断增加, AOF 文件的体积也会变得越来越大。
AOF 重写可以产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。可以减少磁盘占用量,加速数据恢复。
AOF 重写由 Redis 自行触发,bgrewriteaof 仅仅用于手动触发重写操作。在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新旧两个 AOF 文件所保存的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作

AOF的优缺点

  • 优点
    • AOF有序地保存了对数据库执行的所有写入操作,可以对文件进行解析。如果出现了错误的操作,可以对AOF文件解析后通过编辑去除错误的操作,进行数据恢复。
  • 缺点
    • 一般AOF文件的体积大于RDB文件
    • 使用的fsync策略不当的时候可能会降低速度

Redis事务

事务的四大特性:

  • 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;Redis事务并不支持原子性,虽然Redis事务的多个操作是原子的,事务机制也可以保证多个命令的正确执行不被打断,但是Redis事务不支持回滚。
    • DISCARD命令:清除之前放入队列的命令,然后恢复连接状态。
    • WATCH命令: 将特定的key设置为受监控的状态,如果该key被修改,之后的事务调用EXEC时队列中的所有命令均不会执行。调用UNWATCH命令可以接触之前的WATCH操作。
    • Redis命令语法错误:后续的命令即使是正确地添加到命令队列中,所有的命令(命令队列中的所有命令,包括出错命令之前和之后的命令)都不会被执行。
    • Redis命令运行时错误(Redis似乎并不会在加入命令队列时对命令进行类型匹配检查):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      127.0.0.1:6379> multi
      OK
      127.0.0.1:6379> set test1 value1-3
      QUEUED
      127.0.0.1:6379> lpush test2 value2-3
      QUEUED
      127.0.0.1:6379> set test3 value3-3
      QUEUED
      127.0.0.1:6379> exec
      1) OK
      2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
      3) OK
      127.0.0.1:6379> get test1
      "value1-3"
      127.0.0.1:6379> get test3
      "value3-3"
      可以看到,运行时错误只会影响出错的命令执行,其它命令都会正常执行,而不会回滚。
  • 隔离性:一个用户的事务不被其他事务干扰。Redis是单线程的,命令在主线程上串行执行,因此Redis事务可以保证隔离性。
  • 持久性:一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响,除非下一个事务覆盖了这次事务的修改。
  • 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的。

Redis缓存性能问题

  • key的一种设计:表名:列名:主键名:主键值

    缓存穿透

  • 问题:缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,进而需要到数据库上进行查找,同时数据库中也不存在相应的数据。大量的无效请求会落到数据库上造成数据库压力过大。
  • 解决
    • 做好参数校验,在查询前过滤错误的、不合法的参数,向客户端返回错误。校验可以在后端接口层完成,也可以在前端完成。
    • 缓存无效key:在缓存中放置一些无效key,通过这些key进行查询时让缓存返回无意义值或者null。可以防范使用重复无效key的攻击,但是对于大量变化的无效key会占用内存,因此要为无效key设置较短的过期时间。
    • 布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端。
      • 场景:有限的存储空间存储大量需要查找/匹配的元素,提供判断一个元素是否在集合中的功能,允许较低的查找误差。
      • 大致的做法:使用k个hash函数,对于每个要加入的元素,计算出k个hash值。准备一个长度为m的bitmap,然后令这k个hash值对m取模,得到k个在[0, m - 1]范围内的下标,将bitmap中这k个下标对应位置的bit设置为1。对于一个待查找的元素,用同样的k个hash函数计算出k个hash值并对m取模得到k个下标,如果这k个下标对应的位都是1,则说明这个元素可能在集合中,否则,说明这个元素一定不在集合中。布隆过滤器有将不存在的元素判断为在集合中的可能,但是一定不会将存在于集合中的元素判断为不在集合中。
      • 缺点:有误判率、删除困难。降低概率:增加bitmap的大小、调整hash函数

缓存击穿

  • 问题:读取到了缓存中不存在但是数据库中存在的数据,需要向数据库发送请求,并发用户多的时候去数据库读取数据。
  • 解决:
    • 设置热点数据永不过期。
      • 要对数据库发送请求的情况下,加入互斥锁(信号量?),保证只有一定量的请求能够到达数据库。在缓存重生效之前避免数据库崩溃。

缓存雪崩

  • 问题:缓存在同一时间大面积的失效/Redis突然不可用,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。
  • 解决
    • 针对Redis服务不可用的情况:
      • 采用Redis集群
      • 限流(?
    • 针对热点缓存失效的情况:
      • 为不同缓存设置不同的(随机的)生效时间
      • 要对数据库发送请求的情况下,加入互斥锁(信号量?),保证只有一定量的请求能够到达数据库。在缓存重生效之前避免数据库崩溃。
      • 对热点数据不设置过期时间

Redis实现分布式锁

怎么实现分布式锁

  • mysql、zookeeper、redis
  • Redlock算法 -> redisson(Redlock算法的Java实现) lock/unlock
  • 解锁问题:加了锁一定要解锁 -> 怎么删锁?
    • 代码可能出现异常:finally中解锁
    • 整个节点直接炸了,或者网给掐了:过期策略
      • 取得锁后直接为锁添加过期时间:这个操作必须是原子的(否则刚加完锁还没设置过期时间节点直接被扬了,这把锁就不会过期了)。然而redis的setnx命令本身并没有提供直接的timeout参数。
    • 节点从宕机状态中恢复/网络恢复,由于过期策略的存在节点持有的锁有可能已经过期。
      • 该节点会进行一些不持有锁时不应该继续进行的操作
      • 该节点可能会进行锁释放,在redis上删除了别的节点的锁
      • 解决:删除锁时只能删除自己的锁,先尝试获得锁进行判断
        • 这个判断-解锁的操作必须是原子的:否则可能出现在锁过期前一刻判断出锁属于自己,而其它节点在锁过期后获得锁,当前节点判断完成后误删其它节点的锁的情况
        • Redis事务?MULTI EXEC WATCH
        • lua脚本?(Jedis,eval()
      • 锁的过期时间应该确保大于业务执行时间 -> 续期
  • Redis集群分布式锁:主从复制
    • Redis是AP型的(??):有可能存在Redis异步复制造成的锁丢失
      • Master写,Slave读。Redis先直接返回成功信息,再将锁同步到Slave节点。但是复制成功之前Master可能会故障,Master进行降级 -> 锁丢失(似乎并没有什么好的办法,这是Redis方案的固有缺陷)
    • 加锁操作
    • 对比ZK:ZK属于CP型,实行同步复制,先将数据从Master复制到从节点,再返回对外成功消息。此时master故障,新master会保留最新的锁信息。但是这会牺牲可用性(A):对网络延迟敏感,速度取决于最慢速度。

Safety and Liveness

  • Safety: 互斥
  • Liveness
    • A:不会死锁,最终总能获得锁
    • B: 容错性,只要多数的Redis节点还能工作,client总能获取和释放锁。

单实例下的Redis分布式锁实现

加锁

使用SETNX命令:set key value [EX seconds] [PX milliseconds] [NX|XX]。指定过期时间。

  • SET resource_name a_random_value NX PX 30000
  • NX: 只有key不存在的时候才会SET。PX: 以毫秒为单位指定key的过期时间。
  • value: 对于所有的锁请求,这个值必须是唯一的(防止误删)

解锁

  • 无法以单一命令实现判断和解锁,只能使用lua脚本。Redis对Lua脚本的执行具有原子性。
    1
    2
    3
    4
    5
    if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
    else
    return 0
    end
  • 使用EVAL指令可以执行匿名lua脚本:eval [脚本] [key的数量] [arg1, arg2, ...]
  • script load命令:将脚本传送到Redis服务器,Redis服务器返回通过sha1算法得到的哈希值
  • evalsha [哈希值] [key数量] [arg1, arg2, ...]:通过哈希值执行命令

主从模式下用单实例方法有什么问题

  • Redis使用异步复制实现主从复制。
    • A在master上写入了一个锁
    • master在将锁复制到slave之前就crash了
    • salve选出了新的master
    • 新的master并没有A写入的锁,此时B可以在新的master上写入锁,因为key并不存在

秒杀设计

  • 单机情况下的多线程同步只需要使用Java提供的同步机制(synchronized, ReentrantLock…)
  • 多机部署:Nginx负载均衡
  • 多机部署的情况下Java提供的锁机制不够用了 -> 分布式锁(用上Redis提供的分布式锁也没必要使用Java提供的同步机制了

缓存和数据库的数据一致性(读写策略)

 旁路缓存模式(Cache Aside Pattern)

  • 适用场景:需要同时维系DB和cache,读操作较多
  • 写步骤
    • 先更新DB
    • 然后删除cache
  • 读步骤
    • 从cache中读数据,读到就直接返回
    • 如果从cache中读不到,就从DB中读取数据返回
    • 再将数据放到cache中

      常见问题

  • 旁路缓存模式下数据一致性问题是无法避免的(无法完全保证DB和缓存的一致性。
  • 可以先删cache再更新DB吗
    • 不可以。因为这样可能会造成数据库(DB)和缓存(Cache)数据不一致的问题。
    • 例如,两个对同一数据的并发操作,一个是更新操作,另一个是查询操作。更新操作删除缓存后,查询操作没有命中缓存,然后查询操作到数据库中读出了旧数据并将旧数据写入缓存,最后更新操作更新了数据库。此时缓存中的旧数据成为了脏数据(和当前数据库内的值不一致了),如果没有过期的话很可能一直脏下去。
  • 我就是要先删缓存再更新DB有什么解决办法吗?
    • 延时双删策略:先删除cache,然后更新数据库,隔一定时间后再次删除cache,在这段时间内造成的缓存脏数据就会被删除。但是这个间隔时间需要自行进行评估。
  • 这种方式就没有问题吗
    • 理论上也会出现导致数据不一致的case:一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大(至少比先删cache再更新数据库要小得多)。
  • 写操作的时候为什么要删除cache,而不是写cache(?
    • 写操作频繁的时候删除cache确实会影响缓存命中率而导致性能下降。选择更新cache的方式需要考虑安全问题。并发的写操作也可能导致脏数据。
    • 例如:写操作A更新了数据库 -> 写操作B更新了数据库 -> 写操作B更新了缓存 -> 写操作A更新了缓存
  • 如果更新数据库成功而删除缓存失败?
    • 增加cache更新重试机制:如果cache服务当前不可用,就存入队列中等缓存服务可用的时候再将缓存中的key删除。

读写穿透模式(Read/Write Through Pattern)

主从、集群、哨兵

主从

  • conf文件中加入slave的IP和端口
  • 读写分离,master可读写、slave一般只读
  • 将rdb传送给从数据库
  • 异步复制:master执行完写操作立级将结果返回给客户端,再将命令发送给slave

哨兵

集群

  • 主从是全部都放在一个库里