系统设计-数据库篇
池化技术:如何减少频繁创建数据库连接的性能损耗?
它的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本,总之是好处多多。需要注意的是最小连接数和最大连接数。
- 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
- 如果连接池中有空闲连接则复用空闲连接;
- 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
- 如果当前连接数已经大于等于最大连接数,则按照配置中设定的时间(C3P0 的连接池配置是 checkoutTimeout)等待旧的连接可用;
- 如果等待超过了这个设定时间则向用户抛出错误。
查询请求增加时,如何做主从分离?
在发微博的过程中会有些同步的操作,像是更新数据库的操作,也有一些异步的操作,比如说将微博的信息同步给审核系统,所以我们在更新完主库之后,会将微博的 ID 写入消息队列,再由队列处理机依据 ID 在从库中获取微博信息再发送给审核系统。此时如果主从数据库存在延迟,会导致在从库中获取不到微博信息,整个流程会出现异常。
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,纯粹以上面的例子来说,我就有三种解决方案:
第一种方案是数据的冗余。你可以在发送消息队列时不仅仅发送微博 ID,而是发送队列处理机需要的所有微博信息,借此避免从数据库中重新查询数据。
第二种方案是使用缓存。我可以在同步写数据库的同时,也把微博的数据写入到 Memcached 缓存里面,这样队列处理机在获取微博信息的时候会优先查询缓存,这样也可以保证数据的一致性。
最后一种方案是查询主库。我可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成比较大的压力。我会优先考虑第一种方案,因为这种方式足够简单,不过可能造成单条消息比较大,从而增加了消息发送的带宽和时间。
写入数据量增加时,如何实现分库分表?
写入请求量大会造成性能和可用性的问题,如何应对呢?
采取对数据进行"分片",这是一种思想,在数据库中就是分库分表,Kafka中是分区,ES中是分片
分库分表的思想是根据某种分配策略把数据尽量均匀的分到多个数据库节点或多个表中,这样每个数据库节点和表都只存储部分数据,这样对数据的存储、读和写都有意义
存储:因为分库分表后每个节点和表只存储部分数据,这样就能解决数据存储的瓶颈
读:因为每个节点和表存储部分数据,数据量变小,可以提升查询性能
写:数据写入被分摊到多个节点和表,写入性能提高
分库分表有两种方式:垂直拆分和水平拆分
垂直拆分的关注点在业务相关性,原则是按照业务拆分,核心思想是专库专用,将业务耦合度高的拆分到单独库中 水平拆分是把单一数据库按照某种规则拆分到多个数据库和多个数据表中,关注点在数据的特点
水平拆分的两种方法
1.根据某个字段的hash值拆分 比如想把用户表拆成16库64表,方案如下 先对id进行hash操作hash(id),这样有助于打散数据 然后对16取余 hash(id)%16,这样就得到了分库后的索引 最后对64取余 hash(id)%16%64,这样就得到了分表后的索引值
2.根据某个字段的区间或范围拆分 可以根据时间拆分
引入分库分表确实有很多优点,但也会引入新的问题
1.引入了分区分表键,也叫分区键 因为我们需要对分区键进行hash进行索引,这样就导致我们查询都要带上该分区键,比较好的解决办法是用id做分区键,但是如果有根据用户昵称查询的需求怎么办呢? 解决办法就是建立一个昵称和id的映射表
2.一些数据库的特性的实现变得困难 (1)夸库join不可用 解决办法是在业务代码中做处理 (2)求count 采取第三方组件例如redis实现
发号器:如何保证分库分表后ID的全局唯一性?
当数据库分库分表后,使用自增字段就无法保证 ID 的全局唯一性了。
UUID 方案有很大的局限性,也是我不建议你用它的原因,而 twitter 提出的 Snowflake 算法完全可以弥补 UUID 存在的不足,因为它不仅算法简单易实现,也满足 ID 所需要的全局唯一性,单调递增性,还包含一定的业务上的意义
Snowflake 的核心思想是将 64bit 的二进制数字分成若干部分,每一部分都存储有特定含义的数据,比如说时间戳、机器 ID、序列号等等,最终生成全局唯一的有序 ID。它的标准算法是这样的:
从上面这张图中我们可以看到,41 位的时间戳大概可以支撑 pow(2,41)/1000/60/60/24/365 年,约等于 69 年,对于一个系统是足够了。
如果你的系统部署在多个机房,那么 10 位的机器 ID 可以继续划分为 2~3 位的 IDC 标示(可以支撑 4 个或者 8 个 IDC 机房)和 7~8 位的机器 ID(支持 128-256 台机器);12 位的序列号代表着每个节点每毫秒最多可以生成 4096 的 ID。
一般来说我们会有两种算法的实现方式:
一种是嵌入到业务代码里,也就是分布在业务服务器中。这种方案的好处是业务代码在使用的时候不需要跨网络调用,性能上会好一些,但是就需要更多的机器 ID 位数来支持更多的业务服务器。另外,由于业务服务器的数量很多,我们很难保证机器 ID 的唯一性,所以就需要引入 ZooKeeper 等分布式一致性组件来保证每次机器重启时都能获得唯一的机器 ID。
另外一个部署方式是作为独立的服务部署,这也就是我们常说的发号器服务。业务在使用发号器的时候就需要多一次的网络调用,但是内网的调用对于性能的损耗有限,却可以减少机器 ID 的位数,如果发号器以主备方式部署,同时运行的只有一个发号器,那么机器 ID 可以省略,这样可以留更多的位数给最后的自增信息位。即使需要机器 ID,因为发号器部署实例数有限,那么就可以把机器 ID 写在发号器的配置文件里,这样可以保证机器 ID 唯一性,也无需引入第三方组件了。微博和美图都是使用独立服务的方式来部署发号器的,性能上单实例单 CPU 可以达到两万每秒。
分库分表时避免库表分配不均匀:
1. 时间戳不记录毫秒而是记录秒,这样在一个时间区间里可以多发出几个号,避免出现分库分表时数据分配不均。
2. 生成的序列号的起始号可以做一下随机,这一秒是 21,下一秒是 30,这样就会尽量地均衡了。
NoSQL:在高并发场景下,数据库和NoSQL如何做到互补?
1. 在性能方面,NoSQL 数据库使用一些算法将对磁盘的随机写转换成顺序写,提升了写的性能;
2. 在某些场景下,比如全文搜索功能,关系型数据库并不能高效地支持,需要 NoSQL 数据库的支持;
3. 在扩展性方面,NoSQL 数据库天生支持分布式,支持数据冗余和数据分片的特性。例如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 上,减少了数据迁移和验证的成本。
这些都让它成为传统关系型数据库的良好的补充,你需要了解的是,NoSQL 可供选型的种类很多,每一个组件都有各自的特点。你在做选型的时候需要对它的实现原理有比较深入的了解,最好在运维方面对它有一定的熟悉,这样在出现问题时才能及时找到解决方案。
MySql 配合 Elasticsearch 使用。Elasticsearch主要是做搜索用,写还是 MySql,同时发Kafka消息,消费端写ES。ES存在丢数据的问题,所以会定时全量/增量从 MySql 中捞数据,覆写 ES,保证数据的一直性。