1. 什么是缓存击穿
1.1. 介绍
缓存击穿是指某个数据暂时不在缓存里,需要回源到数据库读取,结果同一时间出现了大量的请求压在了 DB 上,进而导致数据库异常拖垮整个服务。
如果你还有印象,我们在讲”读更新、写删除“的时候,就明确说过,这个方法好用,但是存在击穿问题。
1.2. 为什么危害巨大
参考下图:
我们使用缓存的目的有两个,一是加快请求响应,提高性能。二是保护我们的数据库,防止大流量压垮服务。如果出现击穿,则两个目的都无法达到。
缓存击穿是指某个数据暂时不在缓存里,需要回源到数据库读取,结果同一时间出现了大量的请求压在了 DB 上,进而导致数据库异常拖垮整个服务。
如果你还有印象,我们在讲”读更新、写删除“的时候,就明确说过,这个方法好用,但是存在击穿问题。
参考下图:
我们使用缓存的目的有两个,一是加快请求响应,提高性能。二是保护我们的数据库,防止大流量压垮服务。如果出现击穿,则两个目的都无法达到。
缓存穿透指查询一个一定不存在的数据,由于缓存是不命中时被动写,并且处于容错考虑,如果从 DB 查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,失去了缓存的意义。
重要
简而言之,就是有人有意无意的去访问一个眼下不存在的数据。
这个思路简单粗暴,别管有没有数据都直接存到缓存里。
我之前的昵称是Porco1Rosso,名字出自宫崎骏的《红猪》,男人三十一头猪嘛。当然,你也可以叫我老秦。
我来自河南洛阳,小时候在农村长大。早上五六点钟,我爷爷就摇醒我,然后摇醒他的单缸手扶拖拉机,下地干活儿。这个拖拉机啊,便宜实惠,皮实耐操,扩展性特别好,换上不同的挂件儿,可以犁地,翻土,起陇,换个货斗还可以拉货,深受广大劳动人民喜爱。它可比公司的中间件好用太多了,反正我是从来没见我爷爷嫌它难用,骂他是一坨屎。
到了高中的时候,最喜欢的事是周末去考古工地玩儿。那时候,我长得又高又壮,在工地实习的大学生们也爱带着我,扛着洛阳铲满山跑。也可能是这段探墓的经历,让我在业务排障上有一些特别的优势,在上家公司挖掘了各种细枝末节的线索,还原业务场景,避免了百万损失。
租约一般用在微服务场景下,或者分布式缓存里
重要
以下策略有一个大前提:业务场景是读操作远远多于写操作。
租约机制是斯坦福大学在 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;
go 1.24版本之后,对于路由这一块进行了升级改造,但是本质没有大的变化,大家可以搜索看下。
正如我们之前学过的,Go语言搭建一个服务器非常简单,只需要用到几个方法:
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
_, _ = fmt.Fprintf(writer, "关注 香香编程喵喵喵,关注香香编程谢谢喵喵喵!")
})
panic(http.ListenAndServe(":8080", nil))
什么是面向切面编程AOP? - 知乎
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于将横切关注点(Cross-cutting Concerns)与主要业务逻辑分离。横切关注点是指那些存在于应用程序中多个不同部分的功能,例如日志,鉴权等,它们跨越多个模块和对象。
go1.7是一个变动非常大的版本。一方面官方引入了Context来管理多个具有明显父级关系的GoRoutine,一方面对原有的代码包进行了大量的修改,让他们都能够支持Context。
我们之前已经讲过一个用来管理,编排多个GoRoutine的包sync.WaitGroup。它能够解决部分场景,但仍然有一些问题未能解决:
源码地址:https://github.com/saurfang587/xxvote
视频地址:https://space.bilibili.com/3493125102241833/channel/collectiondetail?sid=2310915&ctype=0
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
fmt.Print("香香编程喵喵喵!")
g := gin.Default()
g.GET("/login", func(context *gin.Context) {
fmt.Print("香香编程喵喵喵!")
})
if err := g.Run(":8080"); err != nil {
fmt.Print("启动失败!")
}
}
html css js ->** jquery** -> angularjs->** vue **(dva) react -> uniapp
gin-vue-admin
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>香香编程-投票项目</title>
<script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
</head>
<body>
<main class="main">
<input type="text" name="name" id="name" placeholder="Your name">
<input type="password" name="password" id="password" placeholder="Password">
<button type="submit" id="login_sub">Sign in</button>
</main>
<script>
$(document).ready(function(){
$("#login_sub").on("click",function () {
$.ajax({
//请求资源路径
url:"/login",
//请求参数
data:{
name:$("#name").val(),
password:$("#password").val()
},
//请求方式
type:"post",
//数据形式
dataType:"json",
//请求成功后调用的回调函数
success:function (data) {
console.log(data)
if (data.code !== 0){
alert(data.message)
}else{
alert("已登录")
setTimeout("pageRedirect()", 3000);
}
},
//请求失败后调用的回调函数
error:function () {
alert("登录成功")
}
});
});
});
function pageRedirect() {
window.location.replace("/index"); //实现跳转
}
</script>
</body>
</html>
参数校验:
引入正则表达式,字符串长度,是否是数字,是否是身份证,是否是邮箱,是否是abc开头
func CreateUser(context *gin.Context) {
var user reUser
if err := context.ShouldBind(&user); err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10001,
Message: err.Error(), //这里有风险
})
return
}
//对数据进行校验
if user.Name == "" || user.Password == "" || user.Password2 == "" {
context.JSON(http.StatusOK, tools.ECode{
Code: 10003,
Message: "账号或者密码不能为空", //这里有风险
})
return
}
//校验密码
if user.Password != user.Password2 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10003,
Message: "两次密码不同!", //这里有风险
})
return
}
//校验用户是否存在,这种写法非常不安全。有严重的并发风险
if oldUser := model.GetUser(user.Name); oldUser.Id > 0 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10004,
Message: "用户名已存在", //这里有风险
})
return
}
//判断位数
lenName := len(user.Name)
lenPwd := len(user.Password)
if lenName < 8 || lenName > 16 || lenPwd < 8 || lenPwd > 16 {
context.JSON(http.StatusOK, tools.ECode{
Code: 10005,
Message: "用户名或者密码要大于等于8,小于等于16!", //这里有风险
})
return
}
//密码不能是纯数字
regex := regexp.MustCompile(`^[0-9]+$`)
if regex.MatchString(user.Password) {
context.JSON(http.StatusOK, tools.ECode{
Code: 10006,
Message: "密码不能为纯数字", //这里有风险
})
return
}
//开始添加用户
newUser := model.User{
Name: user.Name,
Password: user.Password,
CreatedTime: time.Now(),
UpdatedTime: time.Now(),
}
if err := model.CreateUser(&newUser); err != nil {
context.JSON(http.StatusOK, tools.ECode{
Code: 10006,
Message: "用户创建失败", //这里有风险
})
return
}
//返回添加成功
context.JSON(http.StatusOK, tools.OK)
return
}
uuid 有什么特性。全局唯一性,唯一ID,用户ID
B站,uid 2 208259 3493125102241833
qq 10000 10001 10002 10003 10007
把主键ID 作为一个用户ID 放出去,合适么?
从产品和运维还有架构等多方面角度 思考一下这个问题。
大意:一个用户X秒内请求超过Y次封Z秒,用户已登录,我能到uid;用户未登录,ip+ua
ip 就不用解释了,计算机网络最基本的知识
ua 指的是 user-agent 用户代理。里面有一些硬件数据。
问题:IP+UA 会出现什么问题?
假如,在你们有个人跟你同款手机,手机系统完全一样,用同一个版本的浏览器,访问同一个网站。ip相同,ua也相同。