mysql知识点
声明:本文章根据 小林coding 图解mysql进行些许改编。侵权删。本文持续更新。 索引篇 什么是索引 当你想查阅书中某个知识的内容,你会选择一页一页的找呢?还是在书的目录去找呢? 傻瓜都知道时间是宝贵的,当然是选择在书的目录去找,找到后再翻到对应的页。书中的目录,就是充当索引的角色,方便我们快速查找书中的内容,所以索引是以空间换时间的设计思想。 那换到数据库中,索引的定义就是帮助存储引擎快速获取数据的一种数据结构,形象的说就是索引是数据的目录。 下图是 MySQL 的结构图,索引和数据就是位于存储引擎中: 索引分类 按「数据结构」分类 B+tree索引、Hash索引、Full-text索引。 按「物理存储」分类 聚簇索引(主键索引)、二级索引(辅助索引)。 主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里; 二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。 所以,在查询时使用了二级索引,如果查询的数据能在二级索引里查询的到,那么就不需要回表,这个过程就是覆盖索引。如果查询的数据不在二级索引里,就会先检索二级索引,找到对应的叶子节点,获取到主键值后,然后再检索主键索引,就能查询到数据了,这个过程就是回表。 按「字段特性」分类 主键索引、唯一索引、普通索引、前缀索引。 唯一约束(UNIQUE KEY)就是一种索引 所以主键就是索引。。 ON DUPLICATE KEY UPDATE 提供无则插入、有则更新 的原子性upsert命令,避免先查再写的并发问题。 常用于实现幂等写入,但不属于乐观锁。 MySQL 在执行 INSERT ... ON DUPLICATE KEY UPDATE 后,会返回一个整数值告诉你有多少行被影响了。 规则如下(重要,正是因为这里容易错): 实际发生的情况 affected_rows 说明 真正插入了一条新记录 1 原来没有这行,插入成功 更新了一条记录,并且数据真的被修改了 2 冲突后走 UPDATE,且字段值确实变了 更新了一条记录,但数据没有任何变化 0 冲突后走 UPDATE,但 SET 后的值和原来一样 按「字段个数」分类 单列索引、联合索引。 B+Tree B+Tree 是一种多叉树,叶子节点才存放数据,非叶子节点只存放索引,而且每个节点里的数据是按主键顺序存放的。每一层父节点的索引值都会出现在下层子节点的索引值中,因此在叶子节点中,包括了所有的索引值信息,并且每一个叶子节点都有两个指针,分别指向下一个叶子节点和上一个叶子节点,形成一个双向链表。 图有误,叶子节点是双向链表 通过主键查询商品数据的过程: 比如,我们执行了下面这条查询语句: ...
分析并发问题的通用方法
概念定义 到底什么是并发 并发 = 多个操作在同一时间窗口内同时进行,无论来自同一人还是不同人。 因为数据只有一份,多人同时操纵就会有风险:谁说了算呢?所以会产生竟态问题。cpu的并发就是最好的理解方式了:cpu核只有一个的情况下,不可能同一时刻给多人使用,所以只能大家轮着时间切片用。 一条数据会不会在同一时刻被两个操作同时修改?是的话就是存在并发问题。 什么是分布式 分布式:系统本身被拆分成了多个服务/节点。系统架构是分散的 一个人讲话给自己听,很难搞错; 三个人ABC一起传话,中间的B听错了,那传到C耳朵里的就变了。大家拿着的信息不一样了,所以分布式场景就是要解决怎么尽量去统一大家信息的问题。 什么是幂等 幂等就是:同一个操作,执行一次和多次,结果不变。 为什么我要单独把他拎出来讲?因为不幂等的设计,既是并发问题,也是分布式问题。 并发如何体现: 多个人同时下单; 一个人同一时刻点赞十次; 分布式如何体现: 项目有三个角色,分别是生产者、MQ、消费者; 还可能是更复杂的微服务项目; 通用分析方法 首先我想声明一句: 在分布式并发问题中,任何不根据具体情景分析,只讲技巧的,都是耍流氓!并发场景信息杂糅、需求各不一致。技术本身是为了解决问题的,脱离了真实情况的技术毫无用处,反而成了心智负担。 四个问题,四个数 遇到一个百万级QPS以内的分布式、并发场景,依次问四个问题,就能精准锁定架构与技术方案方向: 1. 人数 这个操作是 “单人” 还是 “多人”? 单人操作(比如用户修改自己的头像):不会有多人写冲突,只可能有重复提交(浏览器重试),用幂等键就够了(如 Redis SETNX)。 多人操作(比如点赞、关注、下单扣库存):有并发争抢、写冲突风险。 2. 数据库行数 & 表数 这个操作是 “单行单表数据” 还是 “多行 / 跨表数据”? 单行单表(点赞计数、关注关系):数据库层的唯一索引 + 状态字段可以直接解决。 多行或跨表(下单:扣库存→生成订单→扣钱):数据库行锁或乐观锁(version) 是核心,唯一索引只能防重复记录,但防不了库存超卖。 3. 重试数 错误可以 “直接返回” 还是必须 “排队等待”?也就是业务对失败的容忍度如何? 可以直接返回 “请稍后重试”(如点赞冲突、重复领券):用乐观锁或唯一键冲突,返回失败由客户端自行重试。 必须排队串行执行、不能失败丢弃(如秒杀扣库存、限量抢购):用行锁或消息队列串行化,牺牲部分吞吐,强保数据不出错。 4. 一致数 业务需要 “强一致性” 还是 “最终一致性”? 强一致性(转账、钱包扣款、资金交易):必须即时数据一致,不能异步兜底,要用分布式事务、行锁、悲观锁,不能靠异步补偿。 最终一致性(普通下单、发积分、返优惠券、日志统计):允许短暂数据不一致,可异步慢慢补齐,优先用MQ 异步、本地消息表、事务消息解耦,提升吞吐与可用性。 情景演练 情景一:关注 / 取关 问题 答案 人数 多人操作(多人同时对同一博主关注/取关,粉丝数共享) 表数 单行单表(核心socials一行,accounts计数的更新属于事务外 MQ) 重试 直接返回成功(重复操作不报错,与幂等一致) 一致性 核心强一致(关系状态原子翻转),计数最终一致(MQ 异步) ...
并发控制与缓存一致性技术选型之权衡术
核心权衡点:锁的量级与性能损耗的权衡 选择哪种并发控制或一致性手段,本质是在 一致性强度 与 系统吞吐 / 延迟 之间做交易。不存在普适最优解,只有基于回源成本、并发量级和跨实例协调代价的按需组合。 量级轻 → 延迟低,但保护弱:进程内 singleflight,非阻塞软标记,纯 TTL 过期。 量级重 → 一致性强,但代价高:跨实例分布式锁,强同步写穿透。 下面从“数据流”与“控制面”两个维度梳理关键技术,并给出组合原则。 1. 数据一致性模式(如何同步缓存与数据库) 这些模式关注当源数据变更时,缓存如何更新或失效,不直接涉及并发控制,但影响选型。 Cache Aside(旁路缓存) 模式:读未命中则查 DB 并回写缓存;写直接更新 DB,然后删除缓存。 锁量级:写操作无锁,仅单一 DEL;读可配合 singleflight 防击穿。 权衡:最终一致窗口 = 删缓存到下次重建之间;删除失败需重试或 TTL 兜底。 适用:读多写少,可接受短暂不一致的场景(大多数互联网业务)。 Read/Write Through(读写穿透) 模式:缓存层代理所有 DB 读写,业务只与缓存交互。 锁量级:同步写,需保证缓存与 DB 的原子更新,往往引入分布式锁或事务消息。 权衡:一致性强,但每次写都要同时操作缓存+DB,写入延迟高,实现复杂。 Write Behind(异步回写) 模式:写只更新缓存,异步批量刷回 DB。 锁量级:缓存写入轻量,但需要队列/日志保证不丢数据。 权衡:写入性能极高,一致性很弱,允许丢数据窗口。 延迟双删与订阅刷新 延迟双删:写 DB 前先删缓存,DB 更新完成后延迟再删一次(用于规避主从延迟导致的不一致)。 Binlog 订阅(如 Canal):异步监听 DB 变更流水,精确删除或更新缓存。 锁量级:均为异步,无额外锁竞争,但引入消息延迟和架构复杂度。 本系统选择:Cache Aside + MQ 异步删除,利用已有死信队列保障最终一致性,兼顾简单和可靠。 2. 并发控制机制(如何防击穿、防并发重建) singleflight(进程内请求合并) 量级:极轻,无网络开销,仅内存 map + 阻塞等待。 保护范围:单进程内相同请求的合并。 性能损耗:少数请求等待首次执行完毕,延迟可忽略。 适用:高并发下同一批缓存键临时穿透 Redis 的场景。 分布式锁(如 Redis SET NX EX) 量级:重,涉及网络往返、轮询等待、锁 TTL 管理和 Lua 释放。 保护范围:跨实例互斥,确保单点重建。 性能损耗:锁竞争导致额外延迟(轮询 100ms+),可能成为瓶颈。 适用:回源成本极其高昂且要求强一致的场景(如复杂报表、详情缓存重建)。 软标记 / SETNX 提示(非阻塞跨实例通知) 量级:极轻,一次 Redis SETNX 无等待。 保护范围:跨实例“知情权”,让其他实例主动降级而非常规等待。 性能损耗:基本无延迟,仅需判断标记存在与否。 适用:重建允许降级或短暂不一致的场景(批量读、冷缓存预热)。 仅靠 TTL 量级:无。 保护:无任何并发控制。 损耗:零,但可能发生缓存击穿或雪崩。 适用:数据可大量容忍陈旧,或变更极低频。 3. 组合决策矩阵 场景 回源成本 推荐组合 理由 视频实体批量读取(高频、可降级) 中(批量主键查询) singleflight + 软标记 进程内去重 + 跨实例非阻塞防多余穿透 视频详情页读取(中频、不期望旧数据) 中高(复杂 SQL 或关联查询) 分布式锁 + double-check 强控单实例重建,避免多实例重复计算 关注流冷缓存重建(低频、允许最终一致) 高(多表聚合排序) 软标记 + 单一执行 不强制等待,接受少量重复重建成本优于锁等待 写操作后缓存失效(Cache Aside) 极低(DEL 命令) 无需锁,直接 DEL 并发写由 DB 串行化,缓存仅打扫战场 4. 一句话选型指南 先上 singleflight,解决绝大多数进程内并发穿透。 多实例部署且重建轻量时,补 软标记 提示降级,避免引入锁。 只有当“多个实例并发重建会造成无法接受的成本或数据矛盾”时,再引入 分布式锁。 缓存更新一律走 Cache Aside 异步删除,放弃复杂同步,靠重试和 TTL 兜底。 记住:锁是最后的手段,不是默认选项。 能靠最终一致和降级解决的,不要用强同步去惩罚高并发。
悲观锁、乐观锁与三大分布式锁总结
1. 悲观锁与乐观锁 1.1 悲观锁 核心思想:默认一定会存在资源竞争,提前锁定资源独占使用,其他请求阻塞等待,使用完毕再释放锁 特点:先上锁,执行业务,阻塞其他并发请求 1.2 乐观锁 核心思想:默认并发冲突极少发生,不主动加排他锁,所有请求并行执行,出现冲突后再做重试、回滚处理 特点:执行业务在前,冲突处理在后,全程不阻塞其他请求 特别注意:纯版本号乐观锁,作用是解决多人并发修改冲突,无法用于拦截单人重复提交;误用会触发不必要的并发修改异常,业务语义错乱。 2. Redis 分布式锁 异步弱一致、看门狗续命、自旋抢锁、存在丢锁风险 存储唯一uuid标识锁持有者,防止误删除他人锁 2.1 数据结构 2.1.1 简易普通分布式锁(String 类型) 结构:String Key → Value Key:lock:stock:1001 Value:唯一随机 UUID(锁持有者标识) 附带:expire 自动过期时间 无内置版本号,value仅做身份标识,无序无递增时序 2.1.2 Redisson 可重入分布式锁(Hash 类型) Hash Key:lock:stock:1001 Hash Field:客户端ID + 线程ID Hash Value:锁重入次数 附带:全局过期时间 示例: lock:stock:1001 ├─ client001-thread1 : 2 // 重入2次 └─ client002-thread2 : 0 2.2 实现流程 2.2.1 抢锁 生成唯一随机token; 执行原子命令 SETNX key token EX 过期时间 key:锁名称 value:唯一随机标识 NX:不存在才创建 EX:过期时间秒 SET lock:order:1001 uuid_8888 NX EX 30 命令返回成功 = 抢到锁,失败 = 未抢到。 ...
golang并发编程
几个概念 并行和并发 并行 多线程程序在多个核的cpu上运行,就是并行 并发 多线程程序在一个核的cpu上运行,就是并发 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。比如你运行一个 Go 程序 go run main.go,操作系统就会创建一个进程。每个进程有自己独立的内存空间、文件描述符、环境变量等,不同进程之间的资源默认是隔离的。 B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。多个线程共享所属进程的所有资源(内存、文件句柄等) C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。 协程和线程 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。 线程:一个线程上可以跑多个协程,协程是轻量级的线程。 Goroutine 启动单个goroutine goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。 启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。 1func hello() { 2 fmt.Println("Hello Goroutine!") 3} 4func main() { 5 go hello() 6 fmt.Println("main goroutine done!") 7 time.sleep(time.Second) 8} 执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。 启动多个goroutine 1var wg sync.WaitGroup 2 3func hello(i int) { 4 defer wg.Done() // goroutine结束就登记-1 5 fmt.Println("Hello Goroutine!", i) 6} 7func main() { 8 9 for i := 0; i < 10; i++ { 10 wg.Add(1) // 启动一个goroutine就登记+1 11 go hello(i) 12 } 13 wg.Wait() // 等待所有登记的goroutine都结束 14} 若主协程退出,其他任务被迫终止。没有主协程,新协程也不进行。 ...