您好,欢迎来到爱玩科技网。
搜索
您的当前位置:首页Mysql大并发热点行更新的两个骚操作

Mysql大并发热点行更新的两个骚操作

来源:爱玩科技网
Mysql⼤并发热点⾏更新的两个骚操作

要想db操作的性能⾜够⾼,巧妙的设计很重要,事务的操作范围要尽量的⼩。⼀般情况下我们都是使⽤某个orm框架来操作db,这⼀类框架多数的实现⽅式都是夸⽹络多次交互来开启事务上下⽂和执⾏sql操作,是个⿊盒⼦,包括对 autocommit 设置的时机也会有⼀些差异,稍微不注意就会踩坑。

在⼤并发的情况下加上夸⽹络多次交互,就不可避免的由于⽹络延迟、丢包等原因导致事务的执⾏时间过长,出现雪崩概率会⼤⼤增加。建议在性能和并发要求⽐较⾼的场景下尽量少⽤orm,如果⾮要⽤尽量控制好事务的范围和执⾏时间。

⼤并发db操作的原则就是事务操作尽量少跨⽹络交互,⼀旦跨⽹络使⽤事务尽量⽤乐观锁来解决,少⽤悲观锁,尽量缩短当前 session 持有锁的时间。下⾯分享两个在mysql innodb engine 上的⼤并发更新⾏的骚操作,这两个骚操作都是尽可能的缩⼩db锁的范围和时间。

转化update为insert⽐较常见的⼤并发场景之⼀就是热点数据的 update,⽐如具有预算类的库存、账户等。

update从原理上需要innodb engine 先获取row数据,然后进⾏row format转换到mysql服务层,再通过mysql服务器层进⾏数据修改,最后再通过innodb engine写回。

这整个过程每⼀个环节都有⼀定的开销,⾸先需要⼀次innodb查询,然后需要⼀次row format(如果row⽐较宽的话性能损失还是⽐较⼤的),最后还需要⼀次更新和⼀次写⼊,⼤概需要四个⼩阶段。

⼀次update就需要上述四过程开销。此时如果qps⾮常⼤,必然会有⼀定性能开销(这⾥暂不考虑cache、mq之类的削峰)。那么我们能不能将单个⾏的热点分散开来,同时将update转换成insert,我们来看下如何骚操作。

我们引⼊ slot 概念,原来⼀个row 我们通过多个row来表⽰,结果通过sum来汇总。为了不让slot成为瓶颈,我们 rand slot,然后将update转换成insert,通过on duplicate key update ⼦句来解决冲突问题。我们创建⼀个sku库存表。

CREATE TABLE `tb_sku_stock` (

`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `sku_id` bigint(20) NOT NULL, `sku_stock` int(11) DEFAULT '0', `slot` int(11) NOT NULL, PRIMARY KEY (`id`),

UNIQUE KEY `idx_sku_slot` (`sku_id`,`slot`), KEY `idx_sku_id` (`sku_id`)

) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4表中唯⼀性索引 idx_sku_slot ⽤来约束同⼀个 sku_id 不同 slot 。库存增加操作和减少操作要分开来处理,我们先看增加操作。insert into tb_sku_stock (sku_id,sku_stock,slot)values(101010101, 10, round(rand()*9)+1)

on duplicate key update sku_stock=sku_stock+values(sku_stock)

我们给 sku_id=101010101 增加10个库存,通过 round(rand()*9)+1 将slot控制在10个以内(可以根据情况放宽或缩⼩),当 unique key 不冲突的话就⼀直是insert,⼀旦发⽣ duplicate 就会执⾏ update。(update也是分散的)

我们来看下减少库存,减少库存没有增加库存那么简单,最⼤的问题是要做前置检查,不能超扣。我们先看库存总数检查,⽐如我们扣减10个库存数。select sku_id, sum(sku_stock) as ssfrom tb_sku_stock

where sku_id= 101010101

group by sku_id having ss>= 10 for update

mysql的查询是使⽤mvcc来实现⽆锁并发,所以为了实时⼀致性我们需要加上for update来做实时检查。如果库存是够扣减的话我们就执⾏ insert into select 插⼊操作。insert into tb_sku_stock (sku_id, sku_stock, slot)select sku_id,-10 as sku_stock,round(rand() *9+ 1)from(

select sku_id, sum(sku_stock) as ss from tb_sku_stock

where sku_id= 101010101

group by sku_id having ss>= 10 for update) as tmp

on duplicate key update sku_stock= sku_stock+ values(sku_stock)

整个操作都是在⼀次db交互中执⾏完成,如果控制好单表的数据量加上 unique key 配合性能是⾮常⾼的。

消除 select...for update⼤型OLTP系统,都会有⼀些需要周期性执⾏的任务,⽐如定期结算的订单、定期取消的协议等,还有很多兜底的检查、对账程序等都会检查⼀定时间范围内的

状态数据,这些任务⼀般都需要扫描表⾥的某个状态字段。

这些查询基本基于类似status状态字段,由于区分度⾮常低,所以索引基本上在这类场景下没有太⼤作⽤。

为了保证扫描出来的数据不会发⽣并发重复执⾏的问题会对数据加排他锁,通常就是 select...for update,那么这部分数据就不会被重复读取到。但是也就意味着当前db线程将block在此锁上,就是⼀个串⾏操作。

由于是排他锁,数据的 insert、update 都会受到影响,在 repeatable read (可重复读)且没有 unqiue key 的场合下还会触发Gap lock(间隙锁)。我们可以通过⼀个⽅式来消除 select...for update,并且提⾼数据并发处理能⼒。CREATE TABLE `tb_order` (

`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL,

`order_status` int(11) NOT NULL DEFAULT '0', `task_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

我们简单创建⼀个订单表,task_id 是任务id,先让数据结构⽀持多任务并⾏。select order_id from tb_order where order_status=0 limit 10 for update

⼀般做法是通过select...for update 锁住⾏。我们换个⽅法实现同样的效果同时不会存在并发执⾏问题。update tb_order set task_id=10 where order_status=0 limit 10;Query OK, 4 rows affected

select order_id from tb_order where task_id=10 limit 4;

假设我们当前有很多并⾏任务(1-10),假设task_id=10任务执⾏,先update抢占⾃⼰的数据⾏。这个操作基本上在单数ms内,然后再通过select 带上⾃⼰的taskid获取到属于当前task的⾏,同时可以带上准确的limit,因为update是会返回受影响⾏数。

这⾥会有⼀个问题,就是执⾏的task如果由于某个原因终⽌了怎么办,简单⽅法就是⽤⼀个兜底job定期检查超过⼀定时间的task,然后将task_id置为空。作者:王清培(趣头条 Tech Leader)

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- aiwanbo.com 版权所有 赣ICP备2024042808号-3

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务