关于数据库的异步操作

关于数据库的异步操作

为什么需要异步操作?

当服务器同一时刻,某个服务(如登录服务)支持的用户数量成为瓶颈时,使用异步操作可以提升服务器的性能。玩家向服务器提交登录服务,服务器需要查询数据库(玩家的登录数据不会常驻缓存中),向数据库提交查询服务,数据库查询完成后,向服务器返回结果后,服务器向玩家返回登录数据。如果,服务器向数据库提交的查询操作,是同步的,那么在数据库没有返回数据前,服务器处理登录服务的这个线程,将不能继续处理其他玩家的登录服务,这样很容易造成玩家的登录缓慢,有的玩家甚至登录不上,数据库的操作使用异步可以解决这个问题。

数据库异步如何设计?

异步与队列

在java中的常用队列,有ConcurrentLinkedDeque,ArrayBlockingQueue等等。在我设计的这个异步框架中,我使用的是有ConcurrentLinkedDeque。原因:多个生产者,多个消费者,ConcurrentLinkedDeque在并发情况下的性能还不错。在我的这个业务中(如登录服务),假设3000个玩家同时登录,这3000个玩家可能分配到3个线程处理,那么在这个线程中会提交数据库的异步操作到队列,这就是前面说的多个生产者(3个执行登录服务的线程)。多个消费者,是指多个线程(表结构的存储引擎是myisam,每个线程处理指定的一些表,保证原子性)从从异步队列里取出异步任务,然后向数据库提交服务,消费者个数也可以根据队列的任务数量调整。队列的数量应该设置最大上限,以及超过最大上限时应该如何处理。

异步与回调

消费者线程从异步任务队列里取出任务,当任务执行完成后,如果生产者需要根据任务结果执行自己业务,对于这种需求,回调恰好可以解决这个需求。java中有很多回调的例子,如AWT中的事件,想要监听按钮按下的事件 ,实现ActionListener即可。

异步与生产者线程、消费者线程

生产者一秒提交产品的个数

这个理论上是非常快的,只是一个add操作,会慢的地方很大的原因是容器本身的数据结构造成的,我往ConcurrentLinkedDeque添加100w条任务时,几秒钟就完成了。

生产者的个数

这个与当前的业务有关,也和硬件有关。我的计算机CPU有4个逻辑处理器,那么只能跑满(线程满负荷运行)四个生产者,跑满四个后,计算机处理其他的指令时,会很缓慢了。因此,可以在运行环境测试,然后去合理设置这两个值。

消费者一秒消耗产品的个数

这里用数据库的异步操作举列子,消费者从异步任务队列取出任务,然后执行数据库操作,这个过程是消耗IO的,消耗的时间和程序编码有关,也和数据库有关。在确定编码与数据库没问题后,可以用程序测试1s可以执行多少个任务,每个任务执行的最小耗时,最大耗时,平均耗时,每一秒平均执行的任务个数。经过上述的测试后,可以得到每一秒平均执行的任务个数。如果执行完指定的任务数量后,时间还没到1s,可以让该消费者线程休眠指定的时间,休眠完成后,继续执行任务,这个每秒指定任务的数量,最好是取平均值的80%。因此,消费者每秒执行的任务是固定的,cpu的利用率曲线在任务管理器看起来会很平滑,不会是各种正弦曲线。

消费者个数

这个与当前的业务有关,也和硬件有关。如果硬件已经成为瓶颈,可以将这个服务集群。

NOSQL

关系型数据库

Mysql是关系型数据库的代表之一,强大的SQL语句功能与ACID特性。
Mysql支持多种存储引擎,如Innodb,MyIsam.
对事务要求没有要求的话,存储引擎可以使用MyIsam.MyIsam的查询速度远远快于Innodb,下面是我在机器上验证的数据:表中的数据有400w+,表结构使用Innodb时,查询耗时要30ms左右,1s查询个数平均在35个左右;表结构使用Myisam时,查询耗时几乎为0ms,1s中查询5000个左右.
在决定使用哪种存储引擎前,先查看引擎的特性,才能避免踩坑。

关系型数据库的缺点

关系型数据库是一行一行存储
存储与查询操作都是以行进行操作。举个列子,一个表有400W+的数据,一行有10个列,其中一列为年龄,现在要查询年龄大于18岁的所有数据。这个查询过程中,虽然只查询两列的数据,但是还是会查询一行,在数据量大的情况下,这种查询是很耗资源的。插入/更新也是如此,区别在于列的多少,会影响IO的带宽。在数据库服务器那边的更新一列,与更新整行的操作区别不大。

关系数据库的扩展不方便
当业务需求更新后,之前的表结构,不能满足于现在的业务需求,需要增加字段来满足。这个时候,除了更改表结构,还需要去修改应用程序的代码。

不支持集合存储
当要存储List,Map,Set类型时,关系型数据库无法支持。

非关系型数据库
redis

key-val存储,解决了不能存储复杂的数据结构的问题

mongodb

文档型存储,一般是用json格式存储,解决了扩展性、复杂的数据结构问题

非关系型数据库的缺点

不支持事务操作,也不支持关系数据的一些常有操作,如多个表的join操作。

如何选择数据库类型(参考)

NOSQL的出现是为解决关系数据库解决不了的问题,NOSQL也不是银弹,实际应用中需要根据具体业务分析,来选相应的存储方案。
关系型和NoSQL数据库的选型。考虑几个指标,数据量、并发量、实时性、一致性要求、读写分布和类型、安全性、运维性等。根据这些指标,软件系统可分成几类。
1.管理型系统,如运营类系统,首选关系型。
2.大流量系统,如电商单品页的某个服务,后台选关系型,前台选内存型。
3.日志型系统,原始数据选列式,日志搜索选倒排索引。
4.搜索型系统,指站内搜索,非通用搜索,如商品搜索,后台选关系型,前台选倒排索引。
5.事务型系统,如库存、交易、记账,选关系型+缓存+一致性协议,或新型关系数据库。
6.离线计算,如大量数据分析,首选列式,关系型也可以。
7.实时计算,如实时监控,可以选时序数据库,或列式数据库。

异步数据库操作框架实践

选择数据库服务器

Mysql,用这个时间数据库服务器时间比较久,懂一点性能优化,所以我选择这个。

选择持久层框架

Mybatis,第一次用这个框架。

封装数据库异步操作任务
封装异步任务

向数据库提交任务时,将这个任务封装起来,投递到异步队列。根据消费者线程在执行这些操作时需要哪些参数,来封装这个异步任务对象。

对业务层提供提交异步任务的接口

业务层通过提交任务的接口,将任务提交到异步任务队列。业务层提交任务时,要考虑使用多少个生产者合适。

映射异步操作的关系

消费者线程取到这个异步任务对象,怎么区分是那种类型(CRUD)的操作呢?mybatis如何知道调用哪个方法去执行这些操作呢?执行完这些操作后怎么执行回调呢?
如果能把这些关系整合起来,映射成某种关系,那么在消费者线程,就可以根据这种关系,去分辨是哪一种类型的对象,需要如何调用mybaits,mybaits向数据库服务器发送哪种操作,数据库服务器返回后,执行回调。
这个映射的实现使用了java的反射与注解,整个代码的核心在于如何封装异步对象与异步操作的映射关系,mybatis可以解析这种映射关系,可以根据这种映射关系,执行相应的操作,具体请看代码。

总结

今天,这篇文章总结了我在实践过程中的一些心得,其他的不说了,show your code > github

JackLei
JackLei

我是真的不会修电脑

2条评论

卡卡西 发布于12:22 下午 - 9月 5, 2018

可以的

小熊猫 发布于12:38 下午 - 9月 5, 2018

有点厉害