beizhu
type
Post
status
Published
date
Jan 31, 2024
slug
summary
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常用于实现缓存解决方案,提高数据获取速度、降低数据库负载,并支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。
tags
SQL
category
技术
icon
password
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。它通常用于实现缓存解决方案,提高数据获取速度、降低数据库负载,并支持多种数据结构,如字符串、哈希、列表、集合、有序集合等。
Redis使用单线程模型主要因为它是内存数据库,数据存储在内存中,访问速度非常快。Redis的瓶颈通常是网络I/O而不是CPU,因此使用单线程能够简化内部结构,减少上下文切换和锁竞争等性能消耗,从而实现高效率操作。
1. 什么是缓存雪崩、缓存击穿和缓存穿透?怎么解决?
• 缓存雪崩:当大量缓存数据在同一时间过期(失效)或者 Redis 故障宕机时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。

• 缓存击穿:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮。

• 缓存穿透:当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增。

缓存雪崩解决方案:
- 均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免将大量的数据设置成同一个过期时间。我们可以在对缓存数据设置过期时间时,给这些数据的过期时间加上一个随机数,这样就保证数据不会在同一时间过期。
- 互斥锁:当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。
- 后台更新缓存:业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。
缓存击穿解决方案:
- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透解决方案:
- 非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在 API 入口处我们要判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
- 缓存空值或者默认值:当我们线上业务发现缓存穿透的现象时,可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
- 布隆过滤器:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。即使发生了缓存穿透,大量请求只会查询 Redis 和布隆过滤器,而不会查询数据库,保证了数据库能正常运行,Redis 自身也是支持布隆过滤器的。
2. Redis支持哪些数据类型?
- String(字符串):这是Redis最基本的数据类型,一个key对应一个value。String类型是二进制安全的,意味着它可以包含任何数据,如jpg图片或者序列化的对象。一个键最大能存储512MB的数据。这种数据类型常用于缓存一些常用的键值对,以在高并发的情况下提高数据访问速度。
- Hash(哈希):Hash是一个键值对的集合,是一个string类型的field和value的映射表。它特别适合存储对象,可以方便的进行数据的存储和读取。每个Hash可以存储2^32-1个键值对。Hash类型适合存储一些结构化的数据,并且可以对Hash中的某个键值对进行修改、删除操作。
- List(列表):List是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到头部(左边)或者尾部(右边)。这种数据类型可以实现队列和栈的功能,常用于消息队列,并且可以避免消息丢失等问题。
- Set(集合):Set是string类型的无序集合,它是通过哈希表实现的。这种数据类型可以实现去重等功能,例如,你可以使用它来记录一些不重复的值,如用户的ID等。
- Sorted Set(有序集合):Sorted Set和Set相似,但每个字符串元素都会关联一个浮点数类型的分数。元素的分数用来排序,如果两个成员有相同的分数,那么他们的排名按照字典序计算。这种数据类型具有类似Set类型的去重功能,但可以根据分值进行排序。
- Bitmaps(位图)和 HyperLogLogs:这是用于特殊情况的数据类型,例如统计一系列值的“唯一数量”。
3. 布隆过滤器原理是什么?
布隆过滤器(Bloom Filter)是一种空间效率极高的概率型数据结构,它允许你进行快速的成员查询操作。但是布隆过滤器的缺点是它有一定的误报率,意味着有时候会错误地认为某个元素在集合中,而实际上它不在。
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器会通过 3 个操作完成标记:
- 第一步,使用 N 个哈希函数分别对数据做哈希计算,得到 N 个哈希值;
- 第二步,将第一步得到的 N 个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
- 第三步,将每个哈希值在位图数组的对应位置的值设置为 1;
举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。

在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。
布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
- 初始化: 布隆过滤器在开始时是一个由所有位都设置为0的位数组(bit array)或位向量(bit vector),通常表示为一个超长的二进制数。除此之外,还需要几个哈希函数,这些哈希函数可以将任何输入映射到位数组的有效范围内。
- 添加元素(Insertion): 当要添加一个元素时,布隆过滤器会使用每一个哈希函数对该元素进行哈希,从而得到几个哈希值。然后,把这些哈希值对应的位都设置成1。举例来说,如果有三个哈希函数,它们将某个元素分别映射到了第2、第4和第5位,那么这三个位置的位就会被设置成1。
- 成员查询(Query): 当查询一个元素是否存在于布隆过滤器时,会用同样的哈希函数对元素进行哈希,得到几个哈希值,然后检查这些值对应的位是否都是1。如果其中有任何一位不是1,则该元素绝不可能在集合中(因为如果真的存在于集合中的话,在添加时,所有的位都会被设置成1)。如果所有位都是1,则被假定为该元素存在于集合中。但是,由于不同元素的哈希值可能存在冲突,即不同元素的哈希值可能映射到相同的位置,因此即使所有位都是1,也有可能是因为其他元素导致的。
- 误报(False positive): 布隆过滤器允许误报,即假阳性,但不允许假阴性(即如果元素确实在集合中,则查询一定会返回true)。通过增加位数组的大小和使用更多的哈希函数,可以降低误报率,但添加元素的代价也会变得更高,且会增加查询的成本。
布隆过滤器不支持从集合中删除元素,因为那样会影响到其他元素。一种解决办法是使用计数型布隆过滤器(Counting Bloom Filter),它使用计数器数组代替位数组,以支持删除操作。
4. 阐述对Redis的理解,并比较直接在应用程序内存中存储数据与使用Redis作为数据存储的主要差异和潜在优势。
- 数据持久性:
- 应用程序内存: 当应用程序重启或者服务器宕机时,存储在内存中的数据会丢失。
- Redis: 虽然Redis本质上是一个内存数据库,但提供了数据持久化的功能,可以通过RDB(Redis数据库)快照或AOF(Append Only File)日志保持数据持久化。可以把内存中的数据保存到磁盘,即使是在宕机或者重启后,数据也不会丢失。
- 数据结构和操作:
- 应用程序内存: 受限于编程语言提供的数据结构,可能需要自己实现复杂的数据操作。
- Redis: 提供了丰富的数据结构,如字符串、列表、集合、有序集合、散列等,并且对这些数据结构提供了丰富的操作命令。
- 共享数据:
- 应用程序内存: 数据只能被同一个进程或者服务内的线程所访问。
- Redis: 作为独立的服务,可以被多个应用程序或者多个服务器上的应用程序共享。
- 可伸缩性:
- 应用程序内存: 受限于单个应用程序的资源和架构。直接使用应用内存限制了系统的伸缩性,随着数据增长,应用可能会受到物理内存大小的限制。
- Redis: 支持主从复制、哨兵模式和集群模式,可以很好地扩展以满足更高的吞吐量和更大的数据集。
- 网络访问:
- 应用程序内存: 访问数据需要在应用程序的内部进行。
- Redis: 通过网络提供服务,可以简化分布式系统中的数据共享和通信,支持多语言客户端和多个应用实例之间的数据共享。
- 并发处理:
- 应用程序内存: 并发访问和修改数据可能需要开发者手动管理锁和同步机制。
- Redis: 提供了原子操作和其他特性来处理并发访问数据,简化了并发编程的复杂度。
- 高可用性和容错:
- 应用程序内存: 需要开发者自己设计和实现高可用性解决方案。
- Redis: 提供了哨兵(Sentinel)和集群(Cluster)等机制来实现自动故障转移和高可用性。
尽管将数据存储在应用程序内存中可以达到快速读取的效果,使用Redis作为数据存储,能够为应用提供高性能的数据处理能力,并带来更好的数据持久性、丰富的数据结构、高可用性的部署以及方便的扩展性。这些优势使得Redis成为了许多高负载、大数据量且对性能有极高要求的应用程序的首选数据库之一。
5. Redis主从复制的工作原理
Redis的主从复制功能允许将一台Redis服务器的数据复制到一个或多个从服务器,实现数据的同步。工作原理简述如下:
- 当从服务器连接到主服务器时,它会发送一个SYNC命令。
- 主服务器接到SYNC命令后会开始在后台保存快照(BGSAVE),并将这期间接收到的写命令缓存在内存中。
- 快照完成后,主服务器将快照文件和所有积累的写命令发送给从服务器。
- 从服务器首先载入快照文件来恢复数据集,然后执行收到的写命令来与主服务器的数据一致。
之后,主服务器每接到一个写命令,都会将其发送给所有从服务器,让整个复制系统的数据保持一致。通过这种方式,可以实现读写分离,提高系统的可用性。
6. mysql和redis在项目中怎么确保数据一致性的?
对于读数据,选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。
对于写数据,选择更新 db 后,再删除缓存。

针对删除缓存异常的情况,会对 key 设置过期时间兜底,只要过期时间一到,过期的 key 就会被删除了。
除此之外,还有两种方式应对删除缓存失败的情况。
消息队列方案
我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
- 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
- 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
举个例子,来说明重试机制的过程。

订阅 MySQL binlog,再操作缓存
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。
下图是 Canal 的工作原理:

所以,如果要想保证「先更新数据库,再删缓存」策略第二个操作能执行成功,我们可以使用「消息队列来重试缓存的删除」,或者「订阅 MySQL binlog 再操作缓存」,这两种方法有一个共同的特点,都是采用异步操作缓存。