site logo

Keep doing what you want, never give up.

推荐文章
文章配图

基于 MySQL 和 Redis 的缓存穿透、缓存击穿、缓存雪崩及最佳解决方案

为什么缓存优化至关重要? 在高并发系统中,缓存(Redis)能极大地减少数据库(MySQL)查询压力,提升网站性能。然而,如果缓存策略不合理,可能导致 缓存穿透、缓存击穿、缓存雪崩 等经典问题,进而引发数据库崩溃、系统可用性下降、响应变慢。 这些问题不仅影响SEO页面加载速度,还可能导致业务宕机。因此,深入理解Redis缓存的优化策略是架构设计中的关键。本文将详细介绍这些缓存问题的原理、影响,并提供对应的解决方案。 1. 什么是缓存穿透(Cache Penetration)? 缓存穿透: 缓存穿透是指查询的数据在缓存和数据库中都不存在,每次请求都会直接查询数据库还一无所获,导致数据库压力过大,甚至被击穿。通常发生在 恶意攻击、异常数据,导致 Redis 和 MySQL 都查不到数据。 解决方案 ✅ 方法 1:缓存不存在的空值(推荐) 当数据库返回空结果时,将 null 或 empty 值也存入 Redis,防止后续同样的查询持续打穿透到 MySQL 数据库。比如可用如下代码逻辑有效防止缓存穿透: key := "uid:9999999" result := redis.Get(key) if result == nil { user := db.Query("SELECT * FROM users WHERE uid=9999999") if user == nil { redis.Set(key, "empty", 60) // 缓存空值 60 秒 } else { redis.Set(key, user, 600) // 缓存正常数据 10 分钟 } } ✅ 方法 2:布隆过滤器(Bloom Filter)(高效拦截非法请求) 使用布隆过滤器存储所有合法的 uid,请求时先查询布隆过滤器,避免无效查询。 if !bloomFilter.Exists("uid:9999999") { return nil // 直接返回,不查询数据库 } ✅ 方法 3:参数校验 在 API 层面限制非法请求,避免 uid < 0 或超出正常范围的数据进入查询逻辑。
文章配图

理解 MySQL B+ 树索引:磁盘 IO、范围查询与性能优化

MySQL (InnoDB)主要使用 B+ 树索引,而不是哈希、二叉树或B树。 使用B+树的主要原因是B+树对磁盘IO友好,减少了磁盘IO操作,从而提高了查询的速度。 想象你在一本1000页的书中查找某个知识点,如果没有目录(索引),你只能一页一页的翻,这其实就是全书查找(对比到数据库就是全表扫描),这样的查找方式非常慢(效率低下)。但如果这本书有详细的目录那查找起来是不是快了很多?而B+树不仅是有目录,还是多层级目录(根目录只存大概分类、第2层目录存细化分类、。。。、最终数据-叶子节点),这样查询效率自然就高了。 B+树的非叶子节点只存储键值,不存储数据,叶子节点存数据;另外B+树是多叉树(相对二叉树),通常一个节点包含多个子节点,这就减少了树的高度。 举个栗子🌰:如果一个B+树索引有1000个分叉,那差不多只需要3层即可存储10亿条数据,这意味着查询时最多只需要3次磁盘IO。 注意:实际应用中索引键的类型和行数据的大小将直接影响数据的存储量 // InnoDB存储引擎每个节点(页)的大小通常为16KB // 每个索引项由索引键值和指向子节点的指针组成 // 假设键值以 bigint 类型为例,需占用 8 字节,指向子节点的指针占用固定的 6 字节 // 每个索引项的大小 = 8 + 6 = 14字节 每个节点可以存储的索引项数量 = 16KB / (8B + 6B) = 1170 个 // 三层 B+ 树的存储容量计算方法如下: 第一层(根节点):存储 1170 个指向第二层的指针; 第二层(中间节点):每个节点再存储 1170 个指针 = 1170 x 1170 = 1368900 个叶子节点; 第三层(叶子节点):存储实际的数据行指针。 假设每行数据大小为1KB,则每个叶子节点(16KB)可以存储16行数据,那么三层 B+ 树的 总存储容量 = 1368900(叶子节点数)x 16(每个叶子节点存储的数据行数)= 21902400 行, 也就是大约两千一百万行数据。 所以优化MySQL性能的一个可行方案就是控制索引键和要存储数据的大小,如下表,索引键占用的空间越大每个索引页能存储的索引项就越少,从而可能导致B+树的层级增加,以至查询性能下降(磁盘IO次数变多)。 数据类型 占用大小 是否适合索引? 备注 TINYINT 1字节 ✅ SMALLINT 2字节 ✅ INT 4字节 ✅ BIGINT 8字节 ✅ CHAR(10) 10字节 ⚠️ 定长字符串,存储开销比INT大 VARCHAR(255) 变长(最长767字节) ❌ 变长字符串会导致索引存储空间大,影响性能 TEXT/BLOB 不定长(存储在外部页) ❌ MySQL不能直接索引TEXT/BLOB类型 另外也可以通过启动MySQL时指定 –innodb-page-size=32K 或 通过my.cnf修改MySQL默认节点的大小的方式来降低树的高度:
文章配图

Hugo 中 index.md 和 _index.md 的区别,你真的搞懂了吗?

如果你在用 Hugo 建静态站时,有没有经常被 Hugo 的 index.md 和 _index.md 两个文件搞得晕头转向?它们长得这么像,但作用却完全不同。我们今天就来聊聊这两者的区别,以及在实际项目中该怎么使用它们。 要解释index.md与_index.md的区别,需要先弄清楚一个叫Page Bundle的名词。按Hugo官方的解释,Page Bundle就是一个“页面包裹”,把一个页面的内容和相关资源打包在一起,通俗的理解就是用一个目录来管理文章和图片等资源,把一篇文章和文章中的配图一起放在某个目录下,这个目录就是一个Page Bundle。 Page Bundle又分成了两大类,类比一棵树,一种叫叶子Page Budle,另外一种叫分支或树干Page Bundle。那怎么来区分一个Page Bundle到底是树叶,还是树干呢? 这个时候就轮到index.md和_index.md出场了,如果一个目录下包含_index.md文件的就是树干,反之只包含了index.md的目录就是树叶。 content/ ├── leaf/ │ ├── index.md # 表明leaf目录是leaf bundle │ └── leaf.jpg └── branch/ ├── _index.md # 表明branch目录是branch bundle └── branch.jpg 我们都知道树叶是一颗树的最末端,即树叶上不可能再长树干了,同理,包含index.md的目录也不能包含子目录了;而树干是可以继续长出树干和叶子的,所以类比到包含_index.md的目录,它是可以继续嵌套其它目录和子目录的。 如果同时存在 index.md 和 _index.md 的话,Hugo则优先选择使用 _index.md Hugo页面有个方法叫 .Resources,这个方法可以提取出页面相关的所有资源,包括图片、音视频等,它提取的资源就是从对应的Page Bundle中提取出来的。 以上就是index.md和_index.md的主要区别与作用。 应用场景: disableKinds = ["section", "taxonomy", "term"] 想象一下上面的配置,假设你不想让Hugo构建某些类型的页面时,_index.md就派上用场了。虽然页面上没有相关链接,但是咱们管不住那些好奇宝宝啊,用户可能手动输入了某个分类的url但我们又没有让Hugo构建这个分类页面,于是系统就会显示一个系统自带的 404 页面找不到的提示给用户,这让用户感觉你的站点损坏了或者出现了什么未知错误,而使用 _index.md 就可以轻松避免这种尬干的场景。
文章配图

Hugo静态站点部署最佳实践之稀疏检出public静态目录

一个完整的Hugo静态站点目录往往都包含了各种各样的目录,如content内容目录、themes主题目录等等,既然是静态站点,那我们部署最终的代码时最好只将Hugo生成的静态内容部署到生产服务器,也就是public目录。 假设我们本地的Blog目录结构如下所示: /Blog /archetypes /assets /content /Go /Swift /HTML /data /layouts /public /static /themes /GopherLoveApple 我们一般会将Blog目录下的所有内容一并提交到同一个git仓库以防内容丢失或备份,这种情况下我们就可以利用git的稀疏检出(sparse checkout)功能来实现只检出public目录,从而达到只将public目录下的内容部署到线上服务器。 操作也比较简单,具体git命令如下: mkdir www.example.com cd www.example.com git init #在www.example.com空目录下先初始化一个git仓库 git remote add origin <https://github.com/仓库地址.git> #添加远程仓库地址 git config core.sparseCheckout true #开启稀疏检出 echo "public" >> .git/info/sparse-checkout #配置要检出仓库下哪个子目录 git pull origin master 经过上面的配置后当你使用git pull origin master命令拉取代码时就只会拉取public目录下的内容了。
文章配图

大厂面试 - 腾讯:如何避免Go内存逃逸

什么是内存逃逸? 内存逃逸是一种程序错误,它指程序在运行过程中分配了内存,但是之后没有正确的释放这些被分配的内存,从而导致这些内存一直被占用,这就有可能会引起系统内存耗尽,从而导致程序崩溃。 我们都知道程序运行的时候需要将其拷贝到内存中才能执行,而内存空间划分成了不同的区域,其中最重要的两个区域是:堆区 - Heap 和 栈区 - Stack 为什么要分成堆区和栈区呢? 主要是为了满足不同的编程需求: 1、效率:栈区自动管理,适合快速的内存分配和释放,而堆区则适合存储需要长时间存在的大量数据。 2、灵活性:堆区基本可以随心所欲动态的分配内存,适合需要灵活使用大块内存的情况,而栈区刚好相反适合局部短时间使用的情况。 3、内存管理:有助于更好的管理和优化内存使用,提高程序的性能和稳定性。 栈区:用来存放函数调用时的局部变量和函数调用信息的;一般是由系统自动管理的,当函数调用开始时分配内存,函数调用结束时自动释放内存;栈区的分配和释放速度都非常快;栈区一般是有大小限制的,所以如果函数嵌套调用过多,或者局部变量太多也可能会导致栈溢出。 堆区:用于存储动态分配的内存(比如使用new或者malloc等函数分配的内存);特别是C语言中是需要一由程序员手动管理分配和释放的;因为是手动管理分配和释放,所以速度一般比较慢;堆区的大小一般来说比栈区大,可以存储更多的数据。 分配快慢的问题: 栈分配内存只需要用到两个CPU指令,即PUSH分配和RELEASE释放; 堆分配内存首先需要找到一块大小合适的内存块,之后还要通过垃圾回收才能释放。 Go语言为了解决内存逃逸实现了一套内存逃逸分析,所谓逃逸分析就是指程序在编译阶段对代码中哪些变量需要在栈中分配,哪些变量需要在堆上分配进行静态分析的方法。逃逸分析的目的是做到更好内存释放分配,提高程序的运行效率。 Go逃逸分析的源码位于src/cmd/compile/internal/gc/escape.go,感兴趣的话可自行阅读研究,其逃逸分析原理在源码的注释中也已有说明: pointers to stack objects cannot be stored in the heap - 指向栈对象的指针不能存储在堆中 pointers to a stack object cannot outlive that object - 指向栈对象的指针不能超过该对象的生命周期 知道了以上知识点,我们再来看几个实际的例子,通过实例来说明如何避免Go内存逃逸? 1、避免返回函数内部的局部指针变量,比如下面这段代码就会产生内存逃逸: func Sum(a int, b int) *int { total := a + b return &total // 返回了指向局部变量的指针 } func main() { Sum(200, 50) } 这段 Go 代码会产生内存逃逸的原因是由于函数 Sum 返回的是指向局部变量 total 的指针,而这个局部变量会在函数退出时被销毁,因此 Go 会将 total 的内存分配(“迁移”)到堆上,从而避免访问已销毁的内存,这种现象就是内存逃逸(如果返回的指针指向了已经销毁的内存,这将导致访问非法内存,为了避免这种问题,Go 会将这个局部变量 total 从栈上“逃逸”到堆上,确保它在 main 函数结束前仍然有效。)。
文章配图

大厂面试 - 拼多多:Go字典Map

1、Go语言中的Map是有序的还是无序的,为什么? 在Go语言中,字典Map是无序的。 因为 Go 中的 map 是基于 哈希表(hash table)实现的。哈希表的核心思想是通过哈希函数将键映射到特定的位置,然后存储值。由于哈希表是通过散列来决定每个元素的存储位置,元素的存储顺序是 由哈希值和碰撞解决策略 决定的,而不是按插入的顺序排列的。 Go 语言设计者故意没有为 map 提供顺序保证。Go 认为在多数应用场景中,顺序并不重要,尤其是在查找、插入和删除操作的效率比顺序更重要。因此,Go 选择使用无序的哈希表来实现 map,这使得 map 的查找和操作在大多数情况下是 常数时间复杂度 O(1)。 因此,如果使用 for … range 循环遍历的话每次迭代获得的 键 - 元素 对的顺序都可能是不同的。 另外,在Go的内部实现中是先通过一个叫做fastrand()的函数生成一个伪随机数,然后使用这个随机数作为每次遍历的起始桶位置,从而导致每次遍历的起始位置不是固定的。 2、Map字典的键类型不能是哪些类型,应该优先考虑哪些类型作为字典的键类型呢? 这在官方文档中有详细说明: The comparison operators == and != must be fully defined for operands of the key type; thus the key type must not be a function, map, or slice. If the key type is an interface type, these comparison operators must be defined for the dynamic key values; failure will cause a run-time panic. 因此,Go 语言Map字典的键类型不可以是函数、字典和切片。因为 Go 语言的字典类型其实是一个哈希表(hash table)的特定实现,因此其中重要的一个步骤就是需要把键值转换为哈希值,所以,从性能的角度来看的话,“把键值转换为哈希值”以及“把要查找的键值与哈希桶中的键值做对比”, 都是比较耗时的操作。 因此,可以说,求哈希和判等操作的速度越快,对应的类型就越适合作为键类型,应该优先考虑。 3、在值为nil的字典上执行读或写操作会成功吗? 当我们仅声明而不初始化这个字典类型的变量时,这个变量的值就会是nil,比如像下面这样声明的变量mp就是nil。 func main() { var mp map[string]int fmt.Println(mp["key"]) // 这行会输出 0,不会 panic } 在这样的nil字典上执行读操作是没有问题的。但是,如果是写操作(插入)的话,Go语言的运行时系统就会立即抛出一个 panic 致命性错误。 这需要从Map的底层结构来看了,在Go中,Map是引用类型,其底层是由一个 hmap 结构体 表示的,其的定义为: // https://github.com/golang/go/blob/master/src/runtime/map_noswiss.go#L114 // A header for a Go map. type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) clearSeq uint64 extra *mapextra // optional fields } 其中,buckets 指向存储键值对的 “桶数组”,所以,当map声明但未初始化时(即 var mp map[string]int),其值是nil,它的内部结构核心字段值如下:
文章配图

大厂面试 - 腾讯:Go线程模型M:N的理解

1、什么是M、P、G,它们分别代表什么,为什么 Go 会选择 M:N 模型,而不是 1:1 ? M:是操作系统线程,是Machine的缩写,一个M代表一个内核线程,也称工作线程(真正干活的); G:是Go语言的轻量级线程,是Goroutine的缩写,一个G代表一个Go代码片段; P:是Processor的缩写,负责G的调度,一个P代表执行一个Go代码片段所必须的资源; 如果选择 1:1 模型,意味着每个Goroutine都需要创建一个OS线程,这样的话不仅内存开销大,而且系统线程切换的成本也很高,不适合高并发场景;Go运行时是自己管理调度Goroutine,是用户级线程,可在用户动态实现高效的任务切换,从而使得调度更轻量,性能更好。 Go采用协作式调度 + 抢占式调度,这样可以减少OS系统线程切换的成本,P维护本地Goroutine队列,减少全局锁竞争,从而提高了调度效率。 M:N模型即保证了高并发性能,又避免了1:1模型的高成本,这样的设计可以让Go高效的管理成千上万的Goroutine,而不会过度依赖OS线程。 2、为什么 Go 要引入 P,直接用 M 和 G 不行吗? 如果没有P,M直接操作G,那Goroutine调度就会依赖OS系统的线程管理,会有高昂的线程切换开销。而P作为调度器,维护本地Goroutine队列,可以大大减少OS线程的调度开销,提高了性能。 3、Goroutine是如何被调度到M上执行的? P维护着一个本地的Goroutine队列,M通过绑定P获取G来执行。当P的任务队列空了,它会从全局队列或其它P那里“偷取”任务,这样可以保证负载均衡。 4、P的数量可以动态调整吗? P的数量由GOMAXPROCS决定,默认等于CPU核心数,但是可以用runtime.GOMAXPROCS(n)进行动态调整。在高并发情况下,调整P数量可能会影响性能。 5、Go什么时候会创建新的M ? 有Goroutine需要执行但没有空闲M时,Go运行时会创建新的M。 6、使用go关键字生成的goroutine是放置在P中,还是M中? 当一个G被创建并初始化完成后会立即被存储到本地P的runnext字段中,因为G必须先被添加入到P的可以运行G队列中才能在M中运行。 7、Goroutine调度有哪些优化点? 可以使用GOMAXPROCS调整P数量以适配不同的CPU负载;可以让Goroutine自己控制阻塞以减少调度器的上下文切换;可以减少Goroutine创建和销毁的开销(比如用sync.Pool复用)。 8、如何优化Go的高并发任务执行? 控制Goroutine数量,避免因创建过多Goroutine而导致运行时的调度开销增加;适当使用sync.WaitGroup或channel来管理任务的执行顺序;避免过度依赖全局变量,减少锁竞争。
文章配图

Go语言编程性能优化小技巧

虽然 Go 语言是一个高性能的编程语言,但有些情况下我们还是需要关心它的性能优化。 下面便是一些常见的Go性能优化小提示: 字符串处理相关: 使用StringBuffer 或是StringBuild 来拼接字符串,性能会比使用 + 或 +=高三到四个数量级。 如果需要把数字转换成字符串,使用 strconv.Itoa() 比 fmt.Sprintf() 要快一倍左右。 尽可能避免把String转成[]Byte ,这个转换会导致性能下降。 内存分配相关: 如果在 for-loop 里对某个 Slice 使用 append(),请先把 Slice 的容量扩充到位,这样可以避免内存重新分配以及系统自动按 2 的 N 次方幂进行扩展但又用不到的情况,从而避免浪费内存。 GC相关: 避免在热代码中进行内存分配,这样会导致 gc 很忙。尽可能使用 sync.Pool 来重用对象。 锁相关: 使用 lock-free 的操作,避免使用 mutex,尽可能使用 sync/Atomic包。 IO相关: 使用 I/O 缓冲,I/O 是个非常非常慢的操作,使用 bufio.NewWrite() 和 bufio.NewReader() 可以带来更高的性能。 循环相关: 对于在 for-loop 里的固定的正则表达式,一定要使用 regexp.Compile() 编译正则表达式。性能会提升两个数量级。 协议相关: 如果你需要更高性能的协议,可以考虑使用 protobuf 而不是 JSON,因为 JSON 的序列化和反序列化里使用了反射。 字典相关: 你在使用 Map 的时候,使用整型的 key 会比字符串的要快,因为整型比较比字符串比较要快。
文章配图

大厂面试 - 贝壳:JWT是什么?

JWT(JSON Web Token)是一种用于在网络环境中进行安全信息传递的开放标准 完整的JWT由三部分组成,其结构可以表示为xxxxxx.yyyyyy.zzzzzz格式。 头部(Header): 通常包含令牌的类型和加密算法; 载荷(Papload): 包含自定义的数据如用户信息,权限等; 签名(Signature): 作用是用来验证消息的完整性和真实性,防止消息被篡改; JWT是无状态的,服务器不需要为其保存会话信息,从而减轻了服务器的存储压力,但同时也意味着一旦泄露任何请求都会被允许,而常用的解决方法是给JWT设置不固定的过期时间,强制客户端过期重新获取。 JWT另一个特点是,一旦生成后在其过期前都不可再修改其有效期。 它的主要应用场景包括用户身份验证和授权,用户在登录成功后获取JWT后,后续的所有请求都需要携带该JWT向服务器请求资源,服务器则可以根据这个JWT来验证用户的有效性。
文章配图

大厂面试 - 贝壳:MySQL索引的最左前缀原则

1、MySQL索引的最左前缀原则是什么意思?如果有查询条件为 a > 1 and b = 1 and c = 1,请问这个查询能命中MySQL索引吗? 索引的最左前缀原则是指查询语句要使用索引的条件需要满足从关联索引的最左侧开始且需要连续匹配。 比如,如果创建联合索引的顺序是(a, b, c)则上面的查询条件就可以命中索引,但如果创建y索引的顺序是(b,a, c)或者其他顺序,则上面的查询语句则无法使用索引。 2、使用explain工具分析慢查询的时候需要关注哪些信息? 首先关注“type”项,如改项显示为“ALL”则意味着查询使用了全标扫描,这是最差的情况,必须进行优化。如果改项的值为“index”或“range”等则相对较好。 还要注意“row”这项,它代表预计扫描的行数,如果改值较大,即使查询使用到了索引也需要进行优化。 此外,“possible_keys”和“key”这两项也非常重要,前者表示可能使用到的索引,后者表示查询实际使用的索引。如果改项值为空则意味着未使用索引,需要立即优化。