读书_高并发设计40问之二数据库

07,池化技术:如何减少频繁创建数据库连接的性能损耗

用连接池预先建立数据库连接

数据库连接池有两个最重要的配置: 最小连接数和最大连接数, 它们控制着从连接池中获取连接的流程:
01,如果当前连接数小于最小连接数,则创建新的连接处理数据库请求
02,如果连接池中有空闲连接则复用空闲连接
03,如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求
04,如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;如果等待超过了这个设定时间则向用户抛出错误

用线程池预先创建线程

JDK 1.5 中引入的 ThreadPoolExecutor 就是一种线程池的实现,它有两个重要的参数:coreThreadCount 和 maxThreadCount,这两个参数控制着线程池的执行过程。

1
2
3
4
如果线程池中的线程数少于 coreThreadCount 时,处理新的任务时会创建新的线程;  
如果线程数大于 coreThreadCount 则把任务丢到一个队列里面,由当前空闲的线程执行;
当队列中的任务堆积满了的时候,则继续创建线程,直到达到 maxThreadCount;
当线程数达到 maxTheadCount 时还有新的任务提交,那么我们就不得不将它们丢弃了。

这个任务处理流程看似简单,实际上有很多坑
首先, JDK 实现的这个线程池优先把任务放入队列暂存起来,而不是创建更多的线程 ,它比较适用于执行 CPU 密集型的任务,也就是需要执行大量 CPU 运算的任务(可以思考下为什么这么说)。
其次,线程池中使用的队列的堆积量也是我们需要监控的重要指标 ,对于实时性要求比较高的任务来说,这个指标尤为关键(任务被丢给线程池之后,长时间都没有被执行的诡异问题)。
最后, 如果你使用线程池请一定记住 不要使用无界队列(即没有设置固定大小的队列) 。大量的任务堆积会占用大量的内存空间,一旦内存空间被占满就会频繁地触发 Full GC,造成服务不可用。

软件设计思想

这是一种常见的软件设计思想,叫做池化技术, 它的核心思想是空间换时间。它们的创建过程都比较耗时,也比较消耗系统资源 。所以,我们把它们放在一个池子里统一管理起来,以达到 提升性能和资源复用的目的 **。减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本,总之是好处多多。
不过,池化技术也存在一些缺陷,比方说存储
池子中的对象肯定需要消耗多余的内存,如果对象没有被频繁使用,就会造成内存上的浪费。再比方说,池子中的对象需要在系统启动的时候就预先创建完成,这在一定程度上增加了系统启动时间**。

重点:
01,池子的最大值和最小值的设置很重要,初期可以依据经验来设置,后面还是需要根据实际运行情况做调整。
02,池子中的对象需要在使用之前预先初始化完成,这叫做 池子的预热 ,比方说使用线程池时就需要预先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求。
03,池化技术核心是一种空间换时间优化方法的实践,所以要关注空间占用情况,避免出现空间过度使用出现内存泄露或者频繁垃圾回收等问题。

08,数据库优化方案 1:查询请求增加时,如何做主从分离?

主从读写分离
访问模型是 读多写少,读写请求量的差距可能达到几个数量级。

主从读写的两个技术关键点
01,一个是数据的拷贝,我们称为主从复制;

是不是我无限制地增加从库的数量就可以抵抗大量的并发呢? 实际上并不是的。因为随着从库数量增加,从库连接上来的 IO 线程比较多,主库也需要创建同样多的 log dump 线程来处理复制的请求,对于主库资源消耗比较高,同时受限于主库的网络带宽,所以在实际使用中,一般一个主库最多挂 3~5 个从库。

带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响
此时如果主从数据库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息
第一种方案是数据的冗余。
你可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中重新查询数据。
第二种方案是使用缓存。
我可以在同步写数据库的同时,也把微博的数据写入到 Memcached 缓存里面,这样队列处理机在获取微博信息的时候会优先查询缓存,这样也可以保证数据的一致性。
最后一种方案是查询主库。
我可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成比较大的压力。
我会优先考虑第一种方案,因为这种方式足够简单, 不过可能造成单条消息比较大,从而增加了消息发送的带宽和时间 。
缓存的方案比较 适合新增数据的场景,在更新数据的场景下, 先更新缓存可能会造成数据的不一致 ,

02,另一个是,主从分离的情况下,如何屏蔽主从分离带来的访问数据库方式的变化,让开发像使用单一数据库一样。
为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。
第一类以淘宝的 TDDL( Taobao Distributed Data Layer)为代表,以代码形式内嵌运行在应用程序内部
你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将 SQL 语句发给某一个指定的数据源来处理,然后将处理结果返回。
这一类中间件的优点是简单易用,没有多余的部署成本,因为它是植入到应用程序内部,与应用程序一同运行的,所以比较适合运维能力较弱的小团队使用;缺点是缺乏多语言的支持,目前业界这一类的主流方案除了 TDDL,还有早期的网易 DDB,它们都是 Java 语言开发的,无法支持其他的语言。另外,版本升级也依赖使用方更新,比较困难。
另一类是单独部署的代理层方案
这一类方案代表比较多,如早期阿里巴巴开源的 Cobar,基于 Cobar 开发出来的 Mycat,360 开源的 Atlas,美团开源的基于 Atlas 开发的 DBProxy 等等。
这一类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它内部管理着很多的数据源,当有数据库请求时,它会对 SQL 语句做必要的改写,然后发往指定的数据源。
它一般使用标准的 MySQL 通信协议,所以可以很好地支持多语言。由于它是独立部署的,所以也比较方便进行维护升级,比较适合有一定运维能力的大中型团队使用。它的缺陷是所有的 SQL 语句都需要跨两次网络:从应用到代理层和从代理层到数据源,所以在性能上会有一些损耗。

09,数据库优化方案 2:写入数据量增加时,如何实现分库分表?

如何对数据库做垂直拆分

垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的 表拆分到多个不同的数据库中。

如何对数据库做水平拆分

和垂直拆分的关注点不同:垂直拆分的关注点在于 业务相关性,
水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在 数据的特点
拆分的规则有下面这两种:
a,按照某一个字段的哈希值做拆分
b,按照某一个字段的区间来拆分,比较常用的是时间字段
数据库在分库分表之后,数据的访问方式也有了极大的改变,原先只需要根据查询条件到从库中查询数据即可,现在则需要先确认数据在哪一个库表中,再到那个库表中查询数据 。这种复杂度也可以通过数据库中间件来解决

解决分库分表引入的问题
分库分表引入的一个最大的问题就是 引入了分库分表键,也叫做分区键, 也就是我们对数据库做分库分表所依据的字段。
从分库分表规则中你可以看到,无论是哈希拆分还是区间段的拆分,我们首先都需要选取一个数据库字段,这带来一个问题是:我们之后所有的查询都需要带上这个字段,才能找到数据所在的库和表,否则就只能向所有的数据库和数据表发送查询命令。如果像上面说的要拆分成 16 个库和 64 张表,那么一次数据的查询会变成 1664=1024 次查询,查询的性能肯定是极差的。
分库分表引入的*
另外一个问题是一些数据库的特性在实现时可能变得很困难。 比如说多表的 join 在单库时是可以通过一个 SQL 语句完成的,但是拆分到多个数据库之后就无法跨库执行 SQL 了,只能把两个表的数据取出后在业务代码里面做筛选。再比如说在未分库分表之前查询数据总数时只需要在 SQL 中执行 count() 即可,现在数据被分散到多个库表中,我们可能要考虑其他的方案,比方说将计数的数据单独存储在一张表中或者记录在 Redis 里面。
虽然分库分表会对我们使用数据库带来一些不便,但是相比它所带来的扩展性和性能方面的提升,我们
还是需要做的,因为,经历过分库分表后的系统,才能够突破单机的容量和请求量的瓶颈**。

10,发号器:如何保证分库分表后 ID 的全局唯一性?

当我们面临高并发的查询数据请求时,可以使用主从读写分离的方式,部署多个从库分摊读压力;
当存储的数据量达到瓶颈时,我们可以将数据分片存储在多个节点上,降低单个存储节点的存储压力,此时我们的架构变成了下面这个样子:

数据库的主键要如何选择?

一般来讲,两种选择方式:
a,使用业务字段作为主键,比如说对于用户表来说,可以使用手机号,email 或者身份证号作为主键。
b,使用生成的唯一 ID 作为主键
倾向于使用生成的 ID 作为数据库的主键。 不单单是因为它的唯一性,更是因为一旦生成就不会变更,可以随意引用。

基于 Snowflake 算法搭建发号器

疑问:全局唯一性,怎么不提 UUID 呢
首先,生成的 ID 做好具有单调递增性,也就是有序的,而 UUID 不具备这个特点。为什么 ID 要是有序的呢? 因为在系统设计时,ID 有可能成为排序的字段。
另一个原因在于 ID 有序也会提升数据的写入性能。
UUID 不能作为 ID 的另一个原因是它不具备业务含义

其他问题

同一时间位,同一机器,在生成序列号时,是要上锁的吧?
是的 不过像 redis 那样单线程处理就好了

snowflake 不能保证单调递增吧?首先,服务器的时钟可能有快有慢;其次,同一时刻,机器号大的机器生成的 ID 总是大于机器号小的机器,但他的请求可能是先到达了数据库。
首先,服务器的时钟一般是对时的,其次,如果是单独部署的发号器,没有机器 ID 是可以保证单调递增的

关于 41 位的时间戳的可支撑时间问题, 如果时间戳是从 0 开始计算则约可以支持 69 年, 但如果以当前时间开始算, 则可用的只有不到 20 年了( 69-(2019-1970) )
实现时可以不以 1970 年为基准时间

11,NoSQL:在高并发场景下,数据库和NoSQL如何做到互补

对于存储服务来说,我们一般会从两个方面对它做改造:
提升它的读写性能,尤其是读性能,因为我们面对的多是一些读多写少的产品。比方说,你离不开的微信朋友圈、微博和淘宝,都是查询 QPS 远远大于写入 QPS。
增强它在存储上的扩展能力,从而应对大数据量的存储需求。

NoSQL 数据库刚刚被应用时,它被认为是可以替代关系型数据库的银弹,也许因为以下几个方面的原因:
弥补了传统数据库在性能方面的不足;
数据库变更方便,不需要更改原先的数据结构;
适合互联网项目常见的大数据量的场景;

mysql数据库IO问题

以 MySQL 的 InnoDB 存储引擎来说,更新 binlog、redolog、undolog 都是在做顺序 IO,而更新 datafile 和索引文件则是在做随机 IO,而为了减少随机 IO 的发生,关系数据库已经做了很多的优化,比如说写入时先写入内存,然后批量刷新到磁盘上,但是随机 IO 还是会发生。
索引在 InnoDB 引擎中是以 B+ 树(上一节课提到了 B+ 树,你可以回顾一下)方式来组织的,而 MySQL 主键是聚簇索引(一种索引类型,数据与索引数据放在一起),既然数据和索引数据放在一起,那么在数据插入或者更新的时候,我们需要找到要插入的位置,再把数据写到特定的位置上,这就产生了随机的 IO。而且一旦发生了页分裂,就不可避免会做数据的移动,也会极大地损耗写入性能。

使用 NoSQL 提升写入性能

最常见的方案,就是很多 NoSQL 数据库都在使用的 基于 LSM 树的存储引擎, 这种算法使用最多,所以在这里着重剖析一下。
LSM 树(Log-Structured Merge Tree)牺牲了一定的读性能来换取写入数据的高性能,Hbase、Cassandra、LevelDB 都是用这种算法作为存储的引擎。

提升扩展性

像 MongoDB 就有三个扩展性方面的特性。
其一是 Replica,也叫做副本集,你可以理解为主从分离,也就是通过将数据拷贝成多份来保证当主挂掉后数据不会丢失。同时呢,Replica 还可以分担读请求。Replica 中有主节点来承担写请求,并且把对数据变动记录到 oplog 里(类似于 binlog);从节点接收到 oplog 后就会修改自身的数据以保持和主节点的一致。一旦主节点挂掉,MongoDB 会从从节点中选取一个节点成为主节点,可以继续提供写数据服务。
其二是 Shard,也叫做分片,你可以理解为分库分表,即将数据按照某种规则拆分成多份,存储在不同的机器上。MongoDB 的 Sharding 特性一般需要三个角色来支持,一个是 Shard Server,它是实际存储数据的节点,是一个独立的 Mongod 进程;二是 Config Server,也是一组 Mongod 进程,主要存储一些元信息,比如说哪些分片存储了哪些数据等;最后是 Route Server,它不实际存储数据,仅仅作为路由使用,它从 Config Server 中获取元信息后,将请求路由到正确的 Shard Server 中。
其三是负载均衡,就是当 MongoDB 发现 Shard 之间数据分布不均匀,会启动 Balancer 进程对数据做重新的分配,最终让不同 Shard Server 的数据可以尽量的均衡。当我们的 Shard Server 存储空间不足需要扩容时,数据会自动被移动到新的 Shard Server 上,减少了数据迁移和验证的成本。

架构设计和高并发系列
读书_大型网站技术架构01_李智慧
读书_大型网站技术架构02_李智慧
读书_大型网站技术架构03_李智慧
读书_高并发设计40问之一基础
读书_高并发设计40问之二数据库
读书_高并发设计40问之三缓存
读书_高并发设计40问之四消息队列
读书_高并发设计40问之五分布式服务
读书_w3c架构师01通用设计与方法论
读书_w3c架构师02典型架构实践
读书_w3c架构师03数据库与缓存
分布式事务
高并发之缓存
高并发之降级
高并发之限流
数据库_读写分离
消息队列_01消息队列入门
消息队列_02rabbitMQ入门
消息队列_03rabbitMQ安装和使用

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×