知识卡片
分片计数器:把单行互斥锁拆成多行,分散并发写入
内容
一个最直白的计数器实现——用一张表存一行数据,每次点击就
UPDATE cnt = cnt + 1——在低并发下毫无问题,但一旦访问量上来
就会撞上一个具体瓶颈:任何要修改这一行的事务,都要在这条记录
上拿到一把全局互斥锁,这意味着所有并发的更新请求只能排队串行
执行,无论底层数据库并发能力多强,这一个计数器本身就成了
串行化瓶颈。解决思路不是去优化这一行的锁实现,而是从根本上
让”锁”不再集中在同一行上:把一个计数器拆成多行(比如预先建好
100个”槽位”),每次更新时随机选一个槽位去做cnt = cnt + 1,
不同的并发更新请求大概率会落在不同的槽位上,各自持有自己那
一行的锁、互不阻塞;要读取总计数时,只需要对这100行做一次
SUM(cnt)聚合。这个设计的关键洞察是:锁竞争的根源是”多个
并发写操作要抢同一份物理存储”,而不是”计数”这件事本身有什么
并发瓶颈——把物理存储从一行拆成多行,写操作的并发瓶颈就跟着
被拆散了,读操作则通过聚合查询把拆散的数据重新收拢起来,用
一次读时的聚合成本,换取了写时的并发能力。这个思路还可以进一步
扩展:如果需要按天分别计数,可以把”天”也纳入主键,配合
ON DUPLICATE KEY UPDATE语句省去预先建行的步骤;如果担心
槽位表随时间无限增长,还可以周期性地把所有槽位合并回单个槽位、
删除其余槽位,在”分散写入”和”控制行数”之间做取舍。这种”把
一个热点拆成多个冷点,需要时再聚合”的模式,不只适用于计数器,
也是分布式系统里应对写热点的通用套路。
结构图:
flowchart TD
A[单行计数器<br/>UPDATE cnt=cnt+1] --> B[所有并发写请求<br/>争抢同一行互斥锁<br/>被迫串行执行]
C[分片计数器<br/>100个槽位] --> D[每次更新随机选一个槽位<br/>WHERE slot=RAND* 100]
D --> E[不同请求大概率落在<br/>不同槽位,互不阻塞]
E --> F[读取总数时<br/>SELECT SUM cnt 聚合全部槽位]
参考来源
- 位置:《高性能MySQL:第3版》第4章"Schema与数据类型优化"4.4.2节
"计数器表"(源文件:_epub-src/OEBPS/Text/part0011.xhtml)
- 结论依据:原文明确"问题在于,对于任何想要更新这一行的事务
来说,这条记录上都有一个全局的互斥锁(mutex)。这会使得这些
事务只能串行执行。要获得更高的并发更新性能,也可以将计数器
保存在多行中,每次随机选择一行进行更新……要获得统计结果,
需要使用下面这样的聚合查询:SELECT SUM(cnt) FROM
hit_counter",直接说明单行互斥锁的瓶颈及分片+聚合方案的解决
思路。
- 原始内容:问题在于,对于任何想要更新这一行的事务来说,这条
记录上都有一个全局的互斥锁(mutex)。这会使得这些事务只能
串行执行。要获得更高的并发更新性能,也可以将计数器保存在
多行中,每次随机选择一行进行更新。