1. 什么是缓存击穿
1.1. 介绍
缓存击穿是指某个数据暂时不在缓存里,需要回源到数据库读取,结果同一时间出现了大量的请求压在了 DB 上,进而导致数据库异常拖垮整个服务。
如果你还有印象,我们在讲”读更新、写删除“的时候,就明确说过,这个方法好用,但是存在击穿问题。
1.2. 为什么危害巨大
参考下图:
我们使用缓存的目的有两个,一是加快请求响应,提高性能。二是保护我们的数据库,防止大流量压垮服务。如果出现击穿,则两个目的都无法达到。
缓存击穿是指某个数据暂时不在缓存里,需要回源到数据库读取,结果同一时间出现了大量的请求压在了 DB 上,进而导致数据库异常拖垮整个服务。
如果你还有印象,我们在讲”读更新、写删除“的时候,就明确说过,这个方法好用,但是存在击穿问题。
参考下图:
我们使用缓存的目的有两个,一是加快请求响应,提高性能。二是保护我们的数据库,防止大流量压垮服务。如果出现击穿,则两个目的都无法达到。
缓存穿透指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,失去了缓存的意义。
重要
简而言之,就是有人有意无意的去访问一个眼下不存在的数据。
这个思路简单粗暴,别管有没有数据都直接存到缓存里。
租约一般用在微服务场景下,或者分布式缓存里
重要
以下策略有一个大前提:业务场景是读操作远远多于写操作。
租约机制是斯坦福大学在 1989 年提出来的一种方案,主要是为了解决分布式系统中缓存一致性问题的。没错,我们现在使用的技术方案,又是上世纪 90 年代出现的。
在分布式系统中,缓存的使用虽然能提高系统性能,但也带来了数据一致性的挑战。一方面,缓存的存在引入了确保数据一致性的开销和复杂性,降低了部分性能优势。另一方面,分布式系统中的通信延迟、网络故障以及主机故障等问题,使得缓存数据的一致性维护变得更加困难。例如,当多个客户端同时缓存了同一数据,而服务器端的数据发生变化时,如何及时、有效地通知客户端更新缓存,以保证数据的一致性,就是一个亟待解决的问题。
一般成规模的公司,普遍采用的方法。
Binlog 是 MySQL 数据库中的一种二进制日志文件,它记录了数据库中所有的更改操作,包括数据的插入、更新、删除以及数据库结构的修改等。它有一个重要的作用是同步数据,做主从复制,当然了同步数据嘛,也不局限于 MySql,像 ES 啊也是可以做的。
它的格式有很多种,一般用作同步数据的话,会采用 Row 格式。Row 格式则是记录了每一行数据的更改前后的具体内容。比如对于一个更新操作,它会记录更新前的整行数据和更新后的整行数据。这样可以确保主从复制的准确性,避免因 SQL 语句执行环境不同而导致的数据不一致问题,但缺点是日志文件会比较大,因为要记录每一行的详细数据。
关于 kafka 的包,有三个:confluentinc/confluent-kafka-go、IBM/sarama、 kafka-go
前面介绍的所有方法,都无法从根本上解决一个问题:缓存击穿。因此,我们在这里需要先讨论下缓存击穿的问题。
重要
缓存击穿几乎是缓存使用场景中最重要的问题。
缓存击穿是指在缓存系统中,大量请求同时访问一个在缓存中不存在但在数据库等后端存储中存在的数据 key,导致这些请求直接穿透缓存,全部打到后端数据库,从而可能使数据库承受巨大压力,甚至引发系统故障的情况。
之前没有用过重试的话,可以了解下什么是重试。
go get github.com/avast/retry-go
有同学肯定能够想出一个相当靠谱的方式来保证数据的一致性,比如有些同学提出了一个想法:
我先起一个事务,将数据写到数据库里,但是不提交,等到Redis的更新成功以后,再提交事务。如果Redis更新失败,那么我就回滚事务。这样子就可以保证数据库和缓存的一致了。
我只能说,牛啊。提前很多个版本就领悟到了**两阶段提交。**这个思路很好,但是需要写得代码,还有业务上的成本都非常高。并且一定会出现问题:
当缓存不可用时,想要通过接口去修改数据库里的值,怕是再也无法成功了。如果真的出现极端情况。那么有没有又简单又好用的方法?读更新,写删除。
双写策略,是绝大多数同学刚刚入门的时候接触到的一种保证数据一致性的方法,也是最符合人的逻辑的一种方法:既然两个地方存的都有,那我两边都修改一下不就可以了。 这就是双写的思想,简单易懂好实现。
如果你第一时间想到的是这个方法,那说明你是一个正常人……
我们来实现一下,这次先看代码,再看流程图。
func SetBook(w http.ResponseWriter, r *http.Request) {
// 为了便于测试,这里依然采用
if r.Method != http.MethodGet {
http.Error(w, "只支持 Get 请求", http.StatusMethodNotAllowed)
return
}
name := r.URL.Query().Get("name")
id := r.URL.Query().Get("id")
key := fmt.Sprintf("book_%s", id)
//双写策略,同时写Redis和DB
_ = db.Rdb.Set(context.Background(), key, name, time.Second*5).Err()
info := db.Info{}
idInt, _ := strconv.Atoi(id)
info.Save(idInt, name)
// 向客户端发送响应
fmt.Fprintf(w, "双写策略完成。")
}
创建数据库:
CREATE TABLE `info` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`created_at` datetime DEFAULT NULL,
`updated_at` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;