小红书技术REDtech#
这篇文章主要介绍的是小红书数据库团队对于秒杀场景的优化,秒杀场景常见的有排队秒杀的方案,本文则是基于MySQL内核实现的合并秒杀优化。
问题描述#
秒杀场景可以抽象出的最经典的库存扣减模型如下:
| |
随着并发数增加,数据库的update写入性能会急剧下降,最终出现严重卡顿,基本处于不可用的状态(TPS约100-200)
TPS
Transactions Per Second 每秒事务数
下图是Update执行流,其中的红色部分就是瓶颈所在。

- 首先秒杀系统发起请求,通过Myhub/Redhub到达数据库。Myhub/Redhub是小红书的数据库中间件(Proxy),负责连接池管理/路由分发等。
- 在SQL服务层中,MySQL会进行SQL逻辑的处理。
- Update:解析器(Parser)解析 SQL 语法,优化器(Optimizer)决定使用哪个索引(这里通常是主键索引)。
- 预读 (Preread):这是关键一步。要修改数据,必须先找到数据。
- Where 条件过滤:存储引擎把读到的行返回给 Server 层。
- Server 层判断:quantity > 0 吗?如果不满足:通知引擎释放锁,结束。如果满足:进入下一步。
- 数据更新:在内存中将 quantity 的值减 1。
- 写入 Binlog (Binary Log):MySQL的逻辑日志。它需要落盘,因此是瓶颈之一。
- Dump 线程:Binlog 写完后,MySQL 的 Dump 线程会把它推送给下游的 Canal(做缓存同步)、DTS(数据传输)、或者 MySQL 从库(主从复制)。
- 存储层负责数据落盘和事务ACID保证。
- 预读
- MVCC与Btree:通过B+树索引快速定位到数据页,通过MVCC高效读取。
- 死锁检测:给数据加锁前,InnoDB需要遍历这行锁的等待图。这里瓶颈的原因主要是需要遍历。
- 写
- Redolog更新:MySQL的物理日志。为了防止宕机数据丢失,遵循 WAL(Write-Ahead Logging)原则,先把修改操作写到 Redo Log 中。它也需要落盘。
- 行锁竞争:由于MySQL行锁互斥,针对同一行数据,同一时刻只能有一个线程持有锁并进行 Update。这也会导致大量的Lock Wait,成为瓶颈。
- 预读
MVCC
Multi-Version Concurrency Control 多版本并发控制
这是InnoDB用来在高并发场景下同时保持一致性和性能的核心机制之一。它通过为同一行数据维护多个“版本”,让读操作不阻塞写操作、写操作不阻塞读操作。
在InnoDB中,通常读的是历史版本(快照)、写时会生成新版本,这样无需对读加锁。每个InnoDB表中都有三个隐藏字段,当事务修改一行时,旧数据被写入undo log,当前行只保留最新版本,多个版本通过DB_ROLL_PTR串成链表。
| 字段名 | 作用 |
|---|---|
DB_TRX_ID | 最后一次修改该行的事务 ID |
DB_ROLL_PTR | 回滚指针,指向 undo log |
DB_ROW_ID | 行 ID(无主键时才用) |
读的时候由Read View决定,它记录当前系统中活跃事务ID列表,读数据时根据它来判断当前事务应该读哪个版本的数据。实际上,已经存在的读事务,只能看到它创建Read View时已提交的数据;之后新创建的读事务,可以看到更新事务提交后的新数据。
排队秒杀#
回顾刚刚说的瓶颈,最核心的问题是并发竞争,比如同时有1000个线程冲向InnoDB存储层去更新sku_id=1001,它们会导致大量的CPU上下文切换,以及昂贵的死锁检测。
排队秒杀的解决方案是:在MySQL Server层和InnoDB层之间(或者在进入行锁逻辑前)添加一个排队机制。
行锁互斥
行锁互斥主要发生在【当前读/写操作】之间,写操作加锁很容易理解,而普通的MVCC快照读(SELECT)是不参与行锁互斥的。
在InnoDB中,“读”可以分为两类,快照读(Snapshot Read)和当前读(Current Read)。
- 快照读:使用MVCC,不用添加行锁,读的是历史版本,比如
SELECT * FROM t WHERE id = 1;。 - 当前读:当前读必须读最新版本,因此会加行锁,参与互斥,比如:
| |
当SQL进来时,内核根据主键判断大家是否都在改同一行;如果是热点行,就不要直接申请InnoDB的行锁,而是先进队列,这个队列是基于主键Hash的(即Bucket);队列里的线程会先处于挂起状态,然后在统一的时间串行唤醒,按照队列顺序一个一个执行。
这样内核就可以跳过死锁检测的逻辑,同时也能减少锁竞争与上下文切换。但它的局限性在于它依然是串行的,这也是下一阶段优化的原因。

合并秒杀#
方案设计#
合并秒杀将多个事务SQL合并到一个事务进行提交。

具体来说:它通过Leader预读取库存数据写入缓存,然后在缓存中进行Follower库存数据合并扣减,最后Leader一次性将合并数据写入存储引擎。

缓存可见性#
为了实现合并秒杀,需要解决两个问题:
- 数据的可见性:目前MySQL的数据是线程可见的,这样最方便。但是合并秒杀是需要多个线程之间共享数据的。
- 数据一致性问题:Leader-Follower的数据同步问题,要做好状态的流转。
数据可见性通过在表维度添加全局缓存来解决,缓存中会存储当前正在被秒杀的热点数据的快照。
在MySQL内核代码中,每一个打开的表都有一个内存对象,所有访问这个表的线程都能看到这个对象。然后把缓存结构体挂载在这个表的公共内存对象上,这样所有操作这张表的线程(Leader和Followers)都可以通过访问这个公共对象找到这块内存区域,并且缓存的生命周期与表结构一致。

然后是数据一致性,也即Leader-Follower的数据同步问题。
- 首先三个客户端发送了相同的
update语句。经过了Queue PK,由于开启了合并秒杀,跳过了排队秒杀过程- 三个线程开始抢独占锁,最先抢到的将自己标记为Leader,然后读取InnoDB数据和更新数据,将修改后的数据写入全局缓存。Leader做完了工作,释放独占锁,开始进入收集状态,等待若干毫秒
- 另外两个Follower开始抢独占锁。抢到的标记为Follower,然后将全局缓存数据写入线程缓存,然后更新线程缓存完成扣减,最后将线程缓存数据再写入全局缓存。释放独占锁,进入等待唤醒状态。在全局缓存中完成Follower的库存扣减
- 后面的线程依次进入Follower过程,按照读全局缓存->完成扣减->更新全局缓存的过程,依次执行了update语句扣减
- Leader线程完成了收集,重新申请独占锁,将全局缓存数据作为本组最终扣减的值。开始进入2PC过程完成最终数据提交。Leader完成后会唤醒Follower,所有SQL结束
(摘自原文)

行锁优化#
update可以分为两个步骤,一个是收集更新缓存阶段,一个是commit阶段。只有前一个组commit完释放行锁,下一个组才能重新申请,也就是在合并秒杀的内部,组与组之间依然是串行的。但实际上在第一个组进行commit时,第二个组就已经可以开始收集。无需等待第一组commit完成。比如第一组将1000扣减50,那么第二组无需等commit的完成,而可以在第一组提交的时候直接从950开始扣减。其实就跟流水线的思路类似。
这里之所以敢这么做也是因为把它们看成了一个原子的大事务,一起提交或者一起回滚。

Binlog并行提交#

Crash Recovery#
崩溃修复的过程由Binlog和Redolog完成。首先由Binlog生成一个事务集合,然后到Redolog进行对比,判断提交或者回滚。
Redolog
Redolog记录合并前后的值,binlog记录每个事务的改动

这篇文章的优化的本质,实际是将磁盘IO密集型(每次都要写Log)和锁竞争密集型(每次都要抢Mutex)的任务,通过内存批处理的方式,转化为了计算密集型任务(在内存里做加减法),从而绕过了物理硬件的限制。
腾讯技术工程#
这篇文章主要就是了解一下GUI Agent的概念和基础知识了。
GUI Agent通常来说是一个能够看懂屏幕并自动操作的Agent,由以下几部分组成:

它的工作流如下:

得物技术#
核心痛点#
在将 PB 级数据从 ODPS 迁移到自建 Spark/OSS 时,如何高效验证迁移前后数据的一致性?简单的 Join 或 Except 在海量数据下性能极差且无法处理重复数据。
关键技术点#
“MapReduce” 式的 SQL 比对#
放弃高开销的 JOIN,采用 Union ALL + Group By + Sum 的方式。
- 原理:将 A 表标记为
count=1,B 表标记为count_b=1,合并后按主键聚合。 - 判定:
Having sum(count_a) != sum(count_b)即为差异数据。
这实际上就是将“关系型比对”转化为“大数据聚合计算”,进而避免大规模Shuffle。
漏斗式过滤 (Fail-Fast)#
系统设计了三层拦截,层层递进,节省算力:
- 前置校验:检查 Schema、UDF 是否缺失(拦截 50% 无效任务)。
- 指纹校验:通过
SUM(Hash(cols))计算全表指纹,相同时直接跳过详细比对。 - 详细比对:最后才执行全量数据扫描。
