Mastodon
跳过正文
  1. Posts/

每日阅读

·3598 字·8 分钟·
reading - 这篇文章属于一个选集。
§ : 本文

小红书技术REDtech
#

mp.weixin.qq.com
原文链接
小红书MySQL内核秒杀能力重磅再升级

这篇文章主要介绍的是小红书数据库团队对于秒杀场景的优化,秒杀场景常见的有排队秒杀的方案,本文则是基于MySQL内核实现的合并秒杀优化。

问题描述
#

秒杀场景可以抽象出的最经典的库存扣减模型如下:

1
2
3
4
5
6
begin;
insert into inventory_log value (...);
-- 插入库存修改的流水表
update inventory set quantity=quantity-1 where sku_id=? and quantity > 0;
-- 扣减库存表
commit;

随着并发数增加,数据库的update写入性能会急剧下降,最终出现严重卡顿,基本处于不可用的状态(TPS约100-200)

TPS

Transactions Per Second 每秒事务数

下图是Update执行流,其中的红色部分就是瓶颈所在。

开源社区
  1. 首先秒杀系统发起请求,通过Myhub/Redhub到达数据库。Myhub/Redhub是小红书的数据库中间件(Proxy),负责连接池管理/路由分发等。
  2. 在SQL服务层中,MySQL会进行SQL逻辑的处理。
    1. Update:解析器(Parser)解析 SQL 语法,优化器(Optimizer)决定使用哪个索引(这里通常是主键索引)。
    2. 预读 (Preread):这是关键一步。要修改数据,必须先找到数据。
    3. Where 条件过滤:存储引擎把读到的行返回给 Server 层。
      • Server 层判断:quantity > 0 吗?如果不满足:通知引擎释放锁,结束。如果满足:进入下一步。
    4. 数据更新:在内存中将 quantity 的值减 1。
    5. 写入 Binlog (Binary Log):MySQL的逻辑日志。它需要落盘,因此是瓶颈之一。
      • Dump 线程:Binlog 写完后,MySQL 的 Dump 线程会把它推送给下游的 Canal(做缓存同步)、DTS(数据传输)、或者 MySQL 从库(主从复制)。
  3. 存储层负责数据落盘和事务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;
  • 当前读:当前读必须读最新版本,因此会加行锁,参与互斥,比如:
1
2
3
4
SELECT * FROM t WHERE id = 1 FOR UPDATE;
SELECT * FROM t WHERE id = 1 LOCK IN SHARE MODE;
UPDATE t SET ...
DELETE FROM t WHERE ...

当SQL进来时,内核根据主键判断大家是否都在改同一行;如果是热点行,就不要直接申请InnoDB的行锁,而是先进队列,这个队列是基于主键Hash的(即Bucket);队列里的线程会先处于挂起状态,然后在统一的时间串行唤醒,按照队列顺序一个一个执行。

这样内核就可以跳过死锁检测的逻辑,同时也能减少锁竞争与上下文切换。但它的局限性在于它依然是串行的,这也是下一阶段优化的原因。

串行

合并秒杀
#

方案设计
#

合并秒杀将多个事务SQL合并到一个事务进行提交。

合并

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

合并秒杀

缓存可见性
#

为了实现合并秒杀,需要解决两个问题:

  1. 数据的可见性:目前MySQL的数据是线程可见的,这样最方便。但是合并秒杀是需要多个线程之间共享数据的。
  2. 数据一致性问题:Leader-Follower的数据同步问题,要做好状态的流转。

数据可见性通过在表维度添加全局缓存来解决,缓存中会存储当前正在被秒杀的热点数据的快照。

在MySQL内核代码中,每一个打开的表都有一个内存对象,所有访问这个表的线程都能看到这个对象。然后把缓存结构体挂载在这个表的公共内存对象上,这样所有操作这张表的线程(Leader和Followers)都可以通过访问这个公共对象找到这块内存区域,并且缓存的生命周期与表结构一致。

全局缓存

然后是数据一致性,也即Leader-Follower的数据同步问题。

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

(摘自原文)

数据同步

行锁优化
#

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

这里之所以敢这么做也是因为把它们看成了一个原子的大事务,一起提交或者一起回滚。

行锁优化

Binlog并行提交
#

Binlog

Crash Recovery
#

崩溃修复的过程由Binlog和Redolog完成。首先由Binlog生成一个事务集合,然后到Redolog进行对比,判断提交或者回滚。

Redolog

Redolog记录合并前后的值,binlog记录每个事务的改动

崩溃修复

这篇文章的优化的本质,实际是将磁盘IO密集型(每次都要写Log)和锁竞争密集型(每次都要抢Mutex)的任务,通过内存批处理的方式,转化为了计算密集型任务(在内存里做加减法),从而绕过了物理硬件的限制。


腾讯技术工程
#

mp.weixin.qq.com
原文链接
有手就行,教你从0到1快速手搓搭建个GUI Agent

这篇文章主要就是了解一下GUI Agent的概念和基础知识了。

GUI Agent通常来说是一个能够看懂屏幕并自动操作的Agent,由以下几部分组成:

GUIAgent

它的工作流如下:

GUI工作流

得物技术
#

mp.weixin.qq.com
原文链接
Galaxy比数平台功能介绍及实现原理|得物技术

核心痛点
#

在将 PB 级数据从 ODPS 迁移到自建 Spark/OSS 时,如何高效验证迁移前后数据的一致性?简单的 JoinExcept 在海量数据下性能极差且无法处理重复数据。

关键技术点
#

“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)
#

系统设计了三层拦截,层层递进,节省算力:

  1. 前置校验:检查 Schema、UDF 是否缺失(拦截 50% 无效任务)。
  2. 指纹校验:通过 SUM(Hash(cols)) 计算全表指纹,相同时直接跳过详细比对。
  3. 详细比对:最后才执行全量数据扫描。
tinuvile
作者
tinuvile
一个笨小孩
reading - 这篇文章属于一个选集。
§ : 本文