Redis

Redis基础

什么是Redis

Redis是基于C语言开发的NoSQL数据库,它是一种存储KV键值对数据的内存数据库,因此读写速度非常快,被广泛应用于分布式缓存方向。

Redis内置了多种数据类型实现:

  • 5种基础数据类型
    1. String
    2. List
    3. Set
    4. Hash
    5. Zset (有序集合)
  • 3种特殊数据类型
    1. HyperLogLog(基数统计)
    2. Bitmap(位图)
    3. Geospatial (地理位置)

Reids还支持事务、持久化(将内存数据保存在磁盘中,重启时可以再次加载使用)、Lua脚本、多种开箱即用的集群方案(Redis Sentinel、Redis Cluster)。


Redis为什么速度快?

  1. Redis基于内存,内存的访问速度是磁盘的上千倍。

  2. Redis基于Reactor模式,设计开发了一套高效的事件处理模型,主要是单线程事件循环IO多路复用

    image-20231115104527768

  3. Redis内置了多种优化过后的数据类型/结构实现,性能非常高。


Redis持久化机制

使用缓存的时候,我们经常需要对内存中的数据进行持久化,也就是将内存中的数据写入到硬盘中。大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了做数据同步(比如 Redis 集群的主从节点通过 RDB 文件同步数据)。

Redis 支持 3 种持久化方式:

  • 快照(snapshotting,RDB)
  • 只追加文件(append-only file, AOF)
  • RDB 和 AOF 的混合持久化(Redis 4.0 新增)

RDB 持久化

Redis 可以通过创建快照来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

快照持久化是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置:

1
2
3
4
5
save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令

AOF持久化

与快照持久化相比,AOF (append only file)持久化的实时性更好。Redis 6.0 之后默认开启了AOF持久化,可以通过 appendonly 参数开启:

1
appendonly yes

开启 AOF 持久化后,每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到 AOF 缓冲区 server.aof_buf 中,然后再写入到 AOF 文件中(此时还在系统内核缓存区未同步到磁盘),最后再根据持久化方式( fsync策略)的配置来决定何时将系统内核缓存区的数据同步到硬盘中的。

只有同步到磁盘中才算持久化保存了,否则依然存在数据丢失的风险,比如说:系统内核缓存区的数据还未同步,磁盘机器就宕机了,那这部分数据就算丢失了。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof


Redis线程模型

对于读写命令来说,Redis 一直是单线程模型。在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作, Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)


  1. 内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。

    Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外, persist 命令可以移除一个键的过期时间。

  2. 很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。

    如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。


常见的缓存更新策略

Cache Aside Pattern 旁路缓存模式

比较适合读比较多的场景。

Cache Aside Pattern中,服务端需要同时维系数据库和缓存,并且以db为准

  • 步骤:

    1. 更新db

    2. 直接删除cache

      Cache Aside Pattern 旁路缓存模式下,写数据的时候,为什么选择删除cache,而不是更新cache?

      • 对服务端造成资源浪费

        删除cache更直接,如果频繁修改db,就会导致需要频繁更新cache,而cache中的数据可能都没有被访问到。

      • 产生数据不一致问题

        并发场景下,更新cache产生数据不一致问题的概率会更大。

      在写数据的过程中,可以先删除cache,后更新db吗?

      不行,可能会造成数据库和缓存数据不一致

      1. 请求1先把cache中的A数据删除;
      2. 请求2从db中读取数据;
      3. 请求1再把db中的A数据更新。

      在写数据的过程中,先更新db,后删除cache就没问题了吗?

      还是可能出现数据不一致的问题,不过概率很小,因为缓存写入速度比数据库写入速度快很多。

      1. 请求1从db中取数据 (说明此时cache没有缓存该数据)
      2. 请求2更新db中的数据,删除缓存
      3. 请求1将读取的数据写入cache (步骤2很难比步骤3快,因为写数据库一般会先加锁
  • 步骤:

    1. 从cache中读,读取到直接返回
    2. cache中读取不到,就从db读
    3. 把db中读取到的数据放到cache中

Cache Aside Pattern 旁路缓存模式 的缺陷

  1. 首次请求的数据一定不在cache中。

    可以将热点数据提前放入cache。

  2. 如果写操作比较频繁,会导致cache中的数据被频繁删除,影响缓存命中率

    1. 数据库和缓存数据强一致

      加一个锁/分布式锁,更新db的时候,同样更新cache,保证不存在线程安全问题。

    2. 短暂地允许数据库和缓存数据不一致

      更新db的时候,同样更新cache,给缓存加一个比较短的过期时间,即使数据不一致,影响也比较小。


Read/Write Through Pattern 读写穿透模式

读写穿透模式中,服务端把cache视为主要数据存储,从中读取数据并将数据写入其中。cache服务负责将此数据读取和写入db,从而减轻应用程序的职责。

这个模式在平时开发过程中非常少见,大概率是因为Redis没有提供cache将数据写入db的功能。

Read-Through Pattern实际只是在Cache Aside Pattern之上进行了封装。在Cache
Aside Pattern下,发生读请求的时候,如果cache中不存在对应的数据,是由客户端自己负责把数据写入cache,而Read Through Pattern则是cache服务自己来写入缓存的,这对客户端是透明的


Write Behind Pattern 异步缓存写入模式

Write Behind Pattern 异步缓存写入模式 和 Read/Write Through Pattern 读写穿透模式很相似,两者都是由cache服务负责cache和db的读写。

但是读写穿透模式同步更新cache和db,而异步缓存写入模式只是更新缓存,不直接更新db,改为异步批量的方式更新db

这对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新db,但cache服务就挂掉了。

这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少。比如消息队列中消息的异步写入磁盘、MySQL的Innodb Buffer Pool机制都用到了这种策略。

Write Behind Pattern下db的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。