数据库性能瓶颈

对于一些互联网项目来说,企业为节省成本,一般会考虑将所有的数据都存储在一个数据库中,这个时候我们只需要考虑数据库优化、SQL优化、数据缓存、限流,消息队列、服务器性能等问题。

阿里巴巴《Java 开发手册》提出mysql单表行数超过 500 万行后(oracle应该是上千万),数据库性能逐渐下降。

SQL优化

  • 创建必要索引(可以了解下mysql的B+树),通过开启慢查询日志来找出较慢的SQL然后进行相应优化

  • 使用LIMIT对查询结果的记录进行限定,尽量不要拿全表,列表要使用LIMIT来分页,每页数量也不要太大,同时避免SELECT *,使用SELECT a,b,c的方式将需要查找的字段列出来

  • 使用连接JOIN来代替子查询,JOIN的数量一般不允许超过3个,可以通过业务设计来减少JOIN的使用,OR可以改写成IN,OR的效率是n级别,IN的效率是log(n)级别,IN的个数建议控制在200以内

  • 拆分大的DELETE和INSERT语句,sql语句尽可能简单:一条sql只能在一个cpu运算;大语句拆小语句,减少锁时间;一条大sql可以堵死整个库

  • 不做列运算,SELECT id WHERE age + 1 = 10,任何对列的操作都将导致表扫描,它包括数据库教程函数、计算表达式等等,查询时要尽可能将操作移至等号右边,不用函数和触发器,尽量在应用程序实现,使用同类型进行比较,比如用'123'和'123'比,123和123比

  • 尽量避免在WHERE子句中使用LIKE左通配符%xxx的使用(包括%xxx%),<>,NOT IN,!=操作符,否则引擎将放弃使用索引而进行全表扫描

  • 对于连续数值,使用BETWEEN不用IN:SELECT id FROM t WHERE num BETWEEN 1 AND 5

数据缓存

我们可以采用Ehcache、Redis、ES等缓存技术来对一些查询比较频繁的数据进行一个缓存,以降低数据库的压力。缓存解决方案可以查看我的另一篇文章【Redis】缓存常见问题解决思路(缓存穿透、缓存雪崩、缓存击穿)

在这里插入图片描述

限流&消息队列

高并发场景下防止短时间内大量请求操作数据库,我们通常采用限流及消息队列(异步处理)方式来缓解数据库压力。

限流方案请自行查找。

消息队列选择:互联网平台可以选择基于AMQP(高级消息队列协议)、STOMP(简单/流式文本导向的消息传递协议)协议实现的RibbitMQ,ActiveMQ等,物联网平台可以选择基于MQTT(消息队列遥测传输)协议实现的Apache Apollo,EMQ等。

数据库拆分

随着业务场景的变化,大数据量,高并发的挑战下,传统的单数据库优化、缓存等技术已经无法满足业务要求,一个不行就来两,这个时候我们就需要考虑数据库的拆分了:垂直拆分、读写分离、分库分表(水平拆分)。每个拆分过程都能解决业务上的一些问题,但同时也面临了一系列的挑战。

垂直拆分

基于业务层面的垂直拆分:随着系统业务模块的不断增加,单台数据库服务器已无法满足需求,这是我们可以对数据库进行业务层面的拆分,将不同的业务存放到不同的数据库中,从而降低数据库压力。

这就是现在大部分公司使用微服务架构采取的一种方式,将不同的业务模块拆分成一个个独立的微服务,各自连接自己的业务数据库,服务之间通过RMI相互调用(fegin、hession、dubbo、webservice),此处涉及的分布式事务问题将在本文后续讲解。

分库可以是一个服务器上的多个实例库,也可以是多个服务器上的多个实例库。根据业务需求来选择(当然还有成本),数据库性能瓶颈一般首先是 IO,当IO限制比较大时,可以在一个服务器上的不同磁盘下建立不同的数据库实例,当内存限制时就需要在不同服务器上创建各自的实例库。

基于表层面的垂直拆分:当一个表中字段数量过大时候(上百上千),可以将表中很少查询的字段经常查询的字段分离开来,从计算机层面来理解,数据要先读进内存,而内存和硬盘的 IO(分块) 是直接读取一个分页,分页的大小又是固定的,因此查相同的记录数,字段多的表需要交换的分页数就多,时间损耗就更大。

读写分离

垂直拆分可以将业务模块给拆分开来,使得业务模块间互不影响,但当某个业务模块并发量增加后,当前业务模块的数据库将难以支撑所有访问压力,这个时候我们就可以考虑数据库集群方式(mysql主从模式)来实现数据库的读写分离(数据库锁机制可以看出读写分离的好处),以降低单个数据库实例的访问压力,同时保证了服务的高可用。

数据库锁机制

  • 共享锁(读锁):其他事务可以读(具体锁表还是锁行看情况),但不能写。

  • 排他锁(写锁) :其他事务不能读取,也不能写。

数据库主从复制原理:master数据库完成写操作后IO dump线程写入binlog二进制文件中,slave数据库IO线程请求同步,master从master.info获取slave上一次的记录点开始读取并将读取的binlog数据和记录点发送给slave,slave根据记录点将binlog相关的sql语句存放在relay-log,最终slave的sql线程将relay-log里的sql语句应用到库上,至此整个同步过程完成。

数据同步延迟问题:从数据库主从复制原理中我们可以看出,主从数据库数据同步存在延迟,对实时性要求比较高的场景我们可以从master数据库中读取,实时性要求不高的则从slave数据库读取,就比如秒杀系统中的商品的价格必须是实时获取的。

数据库事务问题:在一个事务中包含select 和 insert 的时候,如果读请求走从库,写请求走主库,由于跨了多个库,那么jdbc本地事务已经无法控制,属于分布式事务的范畴。而分布式事务非常复杂且效率较低。因此对于读写分离,目前主流的做法是,事务中的所有sql统一都走主库,由于只涉及到一个库,jdbc本地事务就可以搞定。

水平拆分

读写分离可以保证一个数据库服务的高可用及高负荷,但当一个业务模块中的数据量增加到一定程度的时候,这个时候我们就需要考虑到分表分库了(数据库水平拆分),水平分表从具体实现上又可以分为3种:只分表、只分库、分库分表,oracle 11g后新增了逻辑分表(只分表的特性。

只分表场景:如果库中只有某张表或者少数表数据量过大影响了当前库的性能,那么只需要针对这些表进行拆分,其他表保持不变。

只分库场景:如果库中大部分表数据量都过大,那么可以基于库的拆分,建立多个库,将数据存储在不同实例库中以降低单实例数据库压力。

分库分表场景:假设单表数据量达到1个亿,已经分了5个库,那么每个库表数据量还是有2000W,不增加库的前提下,我们可以对表进行拆分10个,那么每个表数据量就只有200W。

数据库拆分带来的挑战

分布式SQL

分布式sql怎么处理,数据库分库分表后原来的数据库增删改查怎么写,一个用户表user拆分成了3张表user1,user2,user3怎么插入数据。

//拆分前写法
insert into user(id, name) values(1,"张三"),(2,"李四"),(3,"王五");

//拆分后写法
insert into user1(id, name) values(1,"张三");
insert into user2(id, name) values(2,"李四");
insert into user3(id, name) values(3,"王五");

如果一个业务系统数据库拆分后,sql都需要去重写,工作量无疑是巨大的,这时候我们不容易想到的是可以对sql进行一个统一的处理,处理包括SQL解析器SQL路由SQL改写器SQL执行器,下面的实现是伪代码,仅供参考:

//SQL解析器 ====》用于对sql按照某一规则进行解析 如按照id解析
public void sqlParser(String sql) {
    //通过规则引擎解析sql
    parser(sql, ruleEngine);
}
//SQL路由 ====》根据解析结果路由到不同的表、库
public void sqlRoute() {
    if(id % 3 == 0) {
        dbName = user3;
    }else if(id % 2 == 0) {
        dbName = user2;
    }else {
        dbName = user1;
    }
}
//SQL改写器 ====》根据路由结果将SQL进行改写
public void sqlRewriter(String sql) {
    //获取新的SQL
    getNewSql(sql);
}
//SQL执行器 ====》根据改写器获取的SQL分别执行,得到数据结果并合并
public void sqlExecutor() {
    //获取结果
    getReslut();
}

分布式ID

使用分表分库后,我们将不能使用数据库的自增id了,此时我们需要一个全局的ID生成器,分布式ID解决方案可以参考下这篇博客大型互联网公司分布式ID方案总结,其中一个比较轻量级的方案是twitter的snowflake算法。

分布式事务

什么是事务(ACID)?

原子性(Atomicity):原子性是指事务里面的操作要么都成功,要么都不成功。

一致性(Consistency):事务前后数据的完整性必须保持一致。

隔离性(Isolation):多个并发事务之间互不影响。

持久性(Durability):持久性是指一个事务一旦被提交,它对数据库的改变就已经确定了。

数据库拆分后,因为涉及到同时操作多个数据库,所以无法使用数据库的本地事务,我们通常做法是尽量避免分布式事务,一些需要分布式事务的场景通常是可以基于XATCC完成的,有一些第三方的MQ是支持事务消息的,比如RocketMQ。

XA是RM(数据库)层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有数据库的锁,比较耗时。原理基于数据库锁实现,各大数据厂商提供了基于XA协议实现的接口(mysql、oracle都支持)。

TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。(第一阶段的事务就已经提交了,第二阶段执行确认操作或取消第一阶段的操作),不同业务实现方式不一样,需要自己设计。

强一致性:数据更新成功后,任意时刻所有副本中的数据都是一致的,一般采用同步的方式实现。

最终一致性:数据更新成功后,系统不承诺立即可以返回最新写入的值,但是保证最终会返回上一次更新操作的值。

数据库中间件

通过对数据库拆分的了解,我们知道无论是读写分离还是分表分库,对开发人员来说都存在一定的挑战,因此一些有实力的公司或个人就开发出了数据库中间件,开发人员使用了这些中间件后,不论是读写分离还是分库分表,都可以像操作单库单表那样去操作。

主流的数据库中间件设计方案及选型

数据库中间件主要是解决我们上述所看到的分表分库存在的分布式SQL、分布式ID、分布式事务等问题,目前实现的方式有以下两种:

基于服务端数据库代理的:独立服务,支持多语言(java,c,python),阿里巴巴开源的cobar,mycat团队在cobar基础上开发的mycat,mysql官方提供的mysql-proxy,奇虎360在mysql-proxy基础开发的atlas,目前除了mycat,其他服务端中间件项目基本已经没有维护。

基于客户端数据源代理的:通常jar包形式引入,更轻量级,只支持java,阿里巴巴开源的tddl,大众点评开源的zebra,当当网开源的sharding-jdbc。需要注意的是tddl的开源版本只有读写分离功能,没有分库分表,且开源版本已经不再维护。大众点评的zebra开源版本代码已经很久更新,基本上处于停滞的状态。当当网的sharding-jdbc目前算是做的比较好的,代码时有更新,文档资料比较全。

选型:只是java端使用的话推荐sharding-jdbc,性能更好,如果需要支持多语言和方便升级部署的话选用独立服务的mycat,两者都实现读写分离及分表分库,以及基于弱XA(XA二阶段出现问题直接回滚)的分布式事务。

springboot集成sharding-jdbc

springboot集成mycat

分布式数据库

使用分布式数据库后用户就不需要再手动分库分表了。分布式数据库已经完成了这部分工作,目前大部分都是商业数据库,也有开源的,可以自行了解。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐