反应式编程在客户端编程当中的应用相当广泛,而当前在服务端中的应用相对被提及较少。本篇将介绍如何在服务端编程中应用响应时编程来改进数据库操作的性能。

开篇就是结论

接续上一篇《谈反应式编程在服务端中的应用,数据库操作优化,从 20 秒到 0.5 秒》之后,这次,我们带来了关于利用反应式编程进行 upsert 优化的案例说明。建议读者可以先阅读一下前一篇,这样更容易理解本篇介绍的方法。

同样还是利用批量化的思路,将单个 upsert 操作批量进行合并。已达到减少数据库链接消耗从而大幅提升性能的目的。

业务场景

在最近的一篇文章《十万同时在线用户,需要多少内存?——Newbe.Claptrap 框架水平扩展实验》中。我们通过激活多个常驻于内存当中的 Claptrap 来实现快速验证 JWT 正确性的目的。

但,当时有一个技术问题没有得到解决:

Newbe.Claptrap 框架设计了一个特性:当 Claptrap Deactive 时,可以选择将快照立即保存到数据库。因此,当尝试从集群中关闭一个节点时,如果节点上存在大量的 Claptrap ,那么将产生大量的数据库 upsert 操作。瞬间推高数据库消耗,甚至导致部分错误而保存失败。

一点点代码

有了前篇的 IBatchOperator,那么留给这篇的代码内容就非常少了。

首先,按照使用上一篇的 IBatchOperator 编写一个支持操作的 Repository,形如以下代码:

public class BatchUpsert : IUpsertRepository
{
private readonly IDatabase _database;
private readonly IBatchOperator<(int, int), int> _batchOperator; public BatchUpsert(IDatabase database)
{
_database = database;
var options = new BatchOperatorOptions<(int, int), int>
{
BufferCount = 100,
BufferTime = TimeSpan.FromMilliseconds(50),
DoManyFunc = DoManyFunc
};
_batchOperator = new BatchOperator<(int, int), int>(options);
} private Task<int> DoManyFunc(IEnumerable<(int, int)> arg)
{
return _database.UpsertMany(arg.ToDictionary(x => x.Item1, x => x.Item2));
} public Task UpsertAsync(int key, int value)
{
return _batchOperator.CreateTask((key, value));
}
}

然后,只要实现对应数据库的 UpsertMany 方法,便可以很好地完成这项优化。

各种数据库的操作

结合 Newbe.Claptrap 现在项目的实际。目前,被支持的数据库分别有 SQLite、PostgreSQL、MySql 和 MongoDB。以下,分别对不同类型的数据库的批量 Upsert 操作进行说明。

由于在 Newbe.Claptrap 项目中的 Upsert 需求都是以主键作为对比键,因此以下也只讨论这种情况。

SQLite

根据官方文档,使用 INSERT OR REPLACE INTO 便可以实现主键冲突时替换数据的需求。

具体的语句格式形如以下:

INSERT OR REPLACE INTO TestTable (id, value)
VALUES
(@id0,@value0),
...
(@idn,@valuen);

因此只要直接拼接语句和参数调用即可。需要注意的是,SQLite 的可传入参数默认为 999,因此拼接的变量也不应大于该数量。

官方文档:INSERT

PostgreSQL

众所周知,PostgreSQL 在进行批量写入时,可以使用高效的 COPY 语句来完成数据的高速导入,这远远快于 INSERT 语句。但可惜的是 COPY 并不能支持 ON CONFLICT DO UPDATE 子句。因此,无法使用 COPY 来完成 upsert 需求。

因此,我们还是回归使用 INSERT 配合 ON CONFLICT DO UPDATE 子句,以及 unnest 函数来完成批量 upsert 的需求。

具体的语句格式形如以下:

INSERT INTO TestTable (id, value)
VALUES (unnest(@ids), unnest(@values))
ON CONFLICT ON CONSTRAINT TestTable_pkey
DO UPDATE SET value=excluded.value;

其中的 ids 和 values 分别为两个等长的数组对象,unnest 函数可以将数组对象转换为行数据的形式。

注意,可能会出现 ON CONFLICT DO UPDATE command cannot affect row a second time 错误。

因此如果尝试使用上述方案,需要在传入数据库之前,先在程序中去重一遍。而且,通常来说,在程序中进行一次去重可以减少向数据库中传入的数据,这本身也很有意义。

官方文档:unnest 函数
官方文档:Insert 语句

MySql

MySql 与 SQLite 类似,支持 REPLACE 语法。具体语句形式如下:

REPLACE INTO TestTable (id, value)
VALUES
(@id0,@value0),
...
(@idn,@valuen);

官方文档:REPLACE 语句

MongoDB

MongoDB 原生支持 bulkWrite 的批量传输模式,也支持 replace 的 upsert 语法。因此操作非常简单。

那么这里展示一下 C# 操作方法:

private async Task SaveManyCoreMany(
IDbFactory dbFactory,
IEnumerable<StateEntity> entities)
{
var array = entities as StateEntity[] ?? entities.ToArray();
var items = array
.Select(x => new MongoStateEntity
{
claptrap_id = x.ClaptrapId,
claptrap_type_code = x.ClaptrapTypeCode,
version = x.Version,
state_data = x.StateData,
updated_time = x.UpdatedTime,
})
.ToArray(); var client = dbFactory.GetConnection(_connectionName);
var db = client.GetDatabase(_databaseName);
var collection = db.GetCollection<MongoStateEntity>(_stateCollectionName); var upsertModels = items.Select(x =>
{
var filter = new ExpressionFilterDefinition<MongoStateEntity>(entity =>
entity.claptrap_id == x.claptrap_id && entity.claptrap_type_code == x.claptrap_type_code);
return new ReplaceOneModel<MongoStateEntity>(filter, x)
{
IsUpsert = true
};
});
await collection.BulkWriteAsync(upsertModels);
}

这是从 Newbe.Claptrap 项目业务场景中给出的代码,读者可以结合自身需求进行修改。

官方文档:db.collection.bulkWrite ()

通用型解法

优化的本质是减少数据库链接的使用,尽可能在一个链接内完成更多的工作。因此如果特定的数据库不支持以上数据库类似的操作。那么还是存在一种通用型的解法:

  1. 以尽可能快地方式将数据写入一临时表
  2. 将临时表的数据已连表 update 的方式更新的目标表
  3. 删除临时表

UPDATE with a join

性能测试

以 SQLite 为例,尝试对 12345 条数据进行 2 次 upsert 操作。

单条并发:1 分 6 秒

批量处理:2.9 秒

可以在该链接找到测试的代码。

样例中不包含有 MySql、PostgreSQL 和 MongoDB 的样例,因为没有优化之前,在不提高连接池的情况下,一并发基本就爆炸了。所有优化的结果是直接解决了可用性的问题。

所有的示例代码均可以在代码库中找到。如果 Github Clone 存在困难,也可以点击此处从 Gitee 进行 Clone

常见问题解答

此处对一些常见的问题进行解答。

客户端是等待批量操作的结果吗?

这是一个很多网友提出的问题。答案是:是的。

假设我们公开了一个 WebApi 作为接口,由浏览器调用。如果同时有 100 个浏览器同时发出请求。

那么这 100 个请求会被合并,然后写入数据库。而在写入数据库之前,这些客户端都不会得到服务端的响应,会一直等待。

这也是该合并方案区别于普通的 “写队列,后写库” 方案的地方。

原理上讲,这种和 bulkcopy 有啥不一样?

两者是不相关,必须同时才有作用的功能。
首先,代码中的 database.InsertMany 就是你提到的 bulkcopy。

这个代码的关键不是 InsertMany ,而是如何将单次的插入请求合并。
试想一下,你可以在 webapi 上公开一个 bulkcopy 的 API。
但是,你无法将来自不同客户端的请求合并在同一个 API 里面来调用 bulkcopy。
例如,有一万个客户端都在调用你的 API,那怎么合并这些 API 请求呢?

如果如果通过上面这种方式,虽然你只是对外公开了一个单次插入的 API。你却实现了来自不同客户端请求的合并,变得可以使用 bulkcopy 了。这在高并发下很有意义。

另外,这符合开闭的原理,因为你没有修改 Repository 的 InsertOne 接口,却实现了 bulkcopy
的效果。

如果批量操作中一个操作异常失败是否会导致被合并的其他操作全部失败?

如果业务场景是合并会有影响,那当然不应该合并。

批量操作一个失败,当然是一起失败,因为底层的数据库事务肯定也是一起失败。

除非批量接口也支持对每个传入的 ID 做区别对待。典型的,比如 mongodb 的 bulkcopy 可以返回哪些成功哪些失败,那么我们就有能力设置不同的 Tcs 状态。

哪些该合并,哪些不该合并,完全取决于业务。样例给出的是如果要合并,应该怎么合并。不会要求所有都要合并。

Insert 和 Upsert 都说了,那 Delete 和 Select 呢?

笔者笼统地将该模式称为 “反应式批量处理”。要确认业务场景是否应用该模式,需要具备以下这两个基本的要求:

  • 业务下游的批量处理是否会比累积的单条处理要快,如果会,那可以用
  • 业务上游是否会出现短时间的突增频率的请求,如果会,那可以用

当然,还需要考量,比如:下游的批量操作能否却分每个请求的结果等等问题。但以上两点是一定需要考量的。

那么以 Delete 为例:

  • Delete Where In 的速度会比 Delete = 的速度快吗?试一下
  • 会有突增的 Delete 需求吗?想一下

小小工具 Zeal

笔者是一个完整存储过程都写不出来的人。能够查阅到这些数据库的文档,全靠一款名为 Zeal 的离线文档查看免费软件。推荐给您,您也值得拥有。

Zeal 官网地址:https://zealdocs.org/

最后但是最重要!

最近作者正在构建以反应式Actor模式事件溯源为理论基础的一套服务端开发框架。希望为开发者提供能够便于开发出 “分布式”、“可水平扩展”、“可测试性高” 的应用系统 ——Newbe.Claptrap

本篇文章是该框架的一篇技术选文,属于技术构成的一部分。如果读者对该内容感兴趣,欢迎转发、评论、收藏文章以及项目。您的支持是促进项目成功的关键。

如果你对该项目感兴趣,你可以通过 github issues 提交您的看法。

如果您无法正常访问 github issue,您也可以发送邮件到 newbe-claptrap@googlegroups.com 来参与我们的讨论。

点击链接 QQ 交流【Newbe.Claptrap】:https://jq.qq.com/?_wv=1027&k=5uJGXf5

您还可以查阅本系列的其他选文:

GitHub 项目地址:https://github.com/newbe36524/Newbe.Claptrap

Gitee 项目地址:https://gitee.com/yks/Newbe.Claptrap

谈反应式编程在服务端中的应用,数据库操作优化,提速 Upsert的更多相关文章

  1. [转帖]浅谈响应式编程(Reactive Programming)

    浅谈响应式编程(Reactive Programming) https://www.jianshu.com/p/1765f658200a 例子写的非常好呢. 0.9312018.02.14 21:22 ...

  2. atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系

    atitit.组件化事件化的编程模型--服务端控件(1)---------服务端控件与标签的关系 1. 服务器控件是可被服务器理解的标签.有三种类型的服务器控件: 1 1.1. HTML 服务器控件  ...

  3. Java服务端对Cookie的简单操作

    Java服务端对Cookie的简单操作 时间 2016-04-07 10:39:44 极客头条 原文  http://www.cuiyongzhi.com/index.php/post/15.html ...

  4. EF5.0中的跨数据库操作

    以前在用MVC + EF 的项目中,都是一个数据库,一个DbContext,因此一直没有考虑过在MVC+EF的环境下对于多个数据库的操作问题.等到要使用时,才发现这个问题也不小(关键是有个坑).直接说 ...

  5. 【网络编程】服务端产生大量的close_wait状态的进程分析

    首先要明白close_wait状态是在tcp通信四次握手时的一个中间状态: 即当被动关闭方发送完ACK后进入的状态.这个状态的结束,即要达到下一个状态LASK_ACK需要在发无端发送完剩余的数据后(s ...

  6. Java网络编程(TCP服务端)

    /* * TCP服务端: * 1.创建服务端socket服务,并监听一个端口 * 2.服务端为了给客户端提供服务,获取客户端的内容,可以通过accept方法获取连接过来的客户端对象 * 3.可以通过获 ...

  7. 游戏服务端中使用Servlet和Java注解的一个好设计

    SNS类游戏基本都是使用HTTP短连接,用Java来开发服务端时能够使用Servlet+Tomcat非常轻松的架构起服务端来.在这里介绍一种使用Servlet比較好的一种设计,我也见过非常多基于HTT ...

  8. java网络编程-单线程服务端与客户端通信

    该服务器一次只能处理一个客户端请求;p/** * 利用Socket进行简单服务端与客户端连接 * 这是服务端 */public class EchoServer { private ServerSoc ...

  9. 关于调试php的socket服务端中遇到的问题及解决办法

    今天终于把socket的服务端解决了,期间遇到了很多问题呢~ 1.用cmd运行php的问题: 2.socket_create()函数未定义问题: 3.查看端口的问题. 以下逐一说说解决办法: 1.在c ...

随机推荐

  1. [C#.NET拾遗补漏]01:字符串操作

    字符串操作在任意编程语言的日常编程中都随处可见,今天来汇总一下 C# 中关于字符串的一些你可能遗忘或遗漏的知识点. 逐字字符串 在普通字符串中,反斜杠字符是转义字符.而在逐字字符串(Verbatim ...

  2. Physic Design:Floorplan算法概览

    仅用于学习交流,转载请联系本人. 1 floorplan是什么 floorplan常被翻译成布图规划,是指在芯片级别上对模块进行布局,也就是哪个单元放在什么地方,但是单元内部的具体布局并不关心.该步骤 ...

  3. Java实现 LeetCode 773 滑动谜题(BFS)

    773. 滑动谜题 在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示. 一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换. 最终 ...

  4. Java实现 蓝桥杯VIP 算法训练 校门外的树

    问题描述 某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米.我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置:数轴上的每个整数点,即0,1,2,--,L,都种 ...

  5. Java实现 蓝桥杯 历届真题 稍大的串

    串可以按照字典序进行比较.例如: abcd 小于 abdc 如果给定一个串,打乱组成它的字母,重新排列,可以得到许多不同的串,在这些不同的串中,有一个串刚好给定的串稍微大一些.科学地说:它是大于已知串 ...

  6. java实现第N个素数

    素数就是不能再进行等分的整数.比如:7,11.而9不是素数,因为它可以平分为3等份.一般认为最小的素数是2,接着是3,5,... 请问,第100002(十万零二)个素数是多少? 请注意:2 是第一素数 ...

  7. java实现第四届蓝桥杯幸运数

    幸运数 题目描述 幸运数是波兰数学家乌拉姆命名的.它采用与生成素数类似的"筛法"生成. 首先从1开始写出自然数1,2,3,4,5,6,- 1 就是第一个幸运数. 我们从2这个数开始 ...

  8. 02.vue-router的进阶使用

    关键字:路由懒加载,全局导航守卫,组件导航守卫,redirect重定向,keep-alive,params,query 一.目录结构            二.index.js // 配置路由相关的信 ...

  9. 新Mac电脑pycharm爬虫环境安装与配置

    *需要安装的软件:Pycharm.Squel pro.mysql.redis等. 1.下载安装pycharm. 2.下载安装item2. 3.安装brew:'ruby -e "$(curl ...

  10. HashMap常问面试题整理

    去面试时,hashmap总是被经常问的问题,下面总结了几道关于hashmap的问题. 1.hashmap的主要参数都有哪些? 2.hashmap的数据结构是什么样子的?自己如何实现一个hashmap? ...