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

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

要想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 ss
from 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)

原文地址:https://blog.51cto.com/wangqingpei557/2454993

时间: 2024-08-04 09:49:35

大并发热点行更新的两个骚操作的相关文章

干货!IT小伙伴们实用的网站及工具大集合!持续更新!

干货!IT小伙伴们实用的网站及工具大集合!持续更新! Other  崔庆才  4个月前 (12-24)  6720℃  7评论 1.Git 还在担心自己辛辛苦苦写的代码被误删了吗?还在担心自己改错了代码不能挽回吗?还在苦恼于多人开发合作找不到一个好的工具吗?那么用Git就对了,Git是一个开源的分布式版本控制系统,用以有效.高速的处理从很小到非常大的项目版本管理.有了它,代码托管不是问题,版本控制不再苦恼,多人开发变得简单易行. 链接:http://git-scm.com/ 2.GitHub 学

mysql级联更新的两种方式:触发器更新和外键

1.mysql级联更新有两种方式:触发器更新和外键更新. 2.触发器更新和外键更新的目的都是为了保证数据完整性. 我们通常有这样的需求:删除表Table 1中记录,需要同时删除其它表中与Table 1有关的若干记录. 举个例子: 现有2个实体- 麻将机 学生.课程,1种联系- 成绩 分别创建 学生表 students, 课程表course,成绩表score --创建 学生表 students CREATE TABLE IF NOT EXISTS `students` ( `id` int(11)

XCL-Chart刚更新的两个问题(兼容性及内存回收)

刚更新了代码,主要处理两个问题,主要都是某网友帮我测试出来的. 在这先谢了. 问题一. 是在低版本的Android 上,闪退.原因是找不到硬件加速相关的类. 问题二. Demo中用到了Seekbar的三个例子,在滑动时,图有时会消失不见. 对于问题一. 我代码中只有一处地方用到了和硬件加速相关的代码即GraphicalView类,目的是禁掉硬件加速. 原因是我在测试中发现如果开启它.在一些机子上rect显示不出来,另一些则path显示不出来.实在头痛,就将其禁掉了. 因为硬件加速是在3.0 才引

JS小技巧大本事(持续更新)

1. 复制N个字符 1 String.prototype.repeat = function(num){ 2 return (new Array(++num)).join(this); 3 } 4 5 var a = 'A'; 6 a.repeat(5); //'AAAAA' 2. 替代if…else… 1 var result; 2 3 result = isTrue ? something : anotherthing; 4 result = something || anotherthin

mybatis学习之路----批量更新数据两种方法效率对比

原文:https://blog.csdn.net/xu1916659422/article/details/77971696/ 上节探讨了批量新增数据,这节探讨批量更新数据两种写法的效率问题. 实现方式有两种, 一种用for循环通过循环传过来的参数集合,循环出N条sql, 另一种 用mysql的case when 条件判断变相的进行批量更新 下面进行实现. 注意第一种方法要想成功,需要在db链接url后面带一个参数  &allowMultiQueries=true 即:  jdbc:mysql:

oracle MERGE INTO...USING两表关联操作(效率高)

数据量小的时候可以使用子查询做两表关联操作:但数据量大的时候子查询效率太低(因为是单条比对) 比如: update person1 p1 set p1.p_name=(select p_name from person2 where p1.p_id=p2.p_id) where p1.add_date>to_date('2014-09-01','yyyy-mm-dd') 而使用MERGE INTO...USING 作两表关联操作(增.删.改)就效率非常高 MERGE INTO person1 p

TI_DSP_SRIO - 两种SRIO操作模式

DSP SRIO协议的逻辑层定义了操作协议和相应的包格式.DSP上SRIO支持的逻辑层业务(数据发送方法)主要是直接IO/DMA(Direct IO/ Direct Memory Access)和消息传递(Message Passing). ?直接IO/DMA模式是最简单实用的传输方式,其前提是主设备知道被访问端的存储器映射.在这种模式下,主设备可以直接读写从设备的存储器.可以硬件直接实现. ?消息传递模式则类似于以太网的传输方式,它不要求主设备知道被访问设备的存储器状况.数据在被访问设备中的位

hdu5795 A Simple Nim 求nim求法,打表找sg值规律 给定n堆石子,每堆有若干石子,两个人轮流操作,每次操作可以选择任意一堆取走任意个石子(不可以为空) 或者选择一堆,把它分成三堆,每堆不为空。求先手必胜,还是后手必胜。

/** 题目:A Simple Nim 链接:http://acm.hdu.edu.cn/showproblem.php?pid=5795 题意:给定n堆石子,每堆有若干石子,两个人轮流操作,每次操作可以选择任意一堆取走任意个石子(不可以为空) 或者选择一堆,把它分成三堆,每堆不为空.求先手必胜,还是后手必胜. 思路: 组合游戏Nim: 计算出每一堆的sg值,然后取异或.异或和>0那么先手,否则后手. 对于每一堆的sg值求解方法: 设:sg(x)表示x个石子的sg值.sg(x) = mex{sg

批处理完成SVN更新与VS编译的操作

/command:update /command:add /command:commit /logmsg:"msgstr" 多个离散svn目录的更新 "C:/program       files/tortoisesvn/bin/TortoiseProc.exe" /command:update       /Path:"C:/a/b/"*"D:/a/b/c/test/txt" /closeonend:0 /command:u