This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,130 @@
<audio id="audio" title="07 | 池化技术:如何减少频繁创建数据库连接的性能损耗?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/90/79/90169d940ee4b8ac5b84117b58fc4179.mp3"></audio>
在前面几节课程中,我从宏观的角度带你了解了高并发系统设计的基础知识,你已经知晓了,我们系统设计的目的是为了获得更好的性能、更高的可用性,以及更强的系统扩展能力。
那么从这一讲开始我们正式进入演进篇我会再从局部出发带你逐一了解完成这些目标会使用到的一些方法这些方法会针对性地解决高并发系统设计中出现的问题。比如在15讲中我会提及布隆过滤器这个组件就是为了解决存在大量缓存穿透的情况下如何尽量提升缓存命中率的问题。
当然,单纯地讲解理论,讲解方案会比较枯燥,所以我将用一个虚拟的系统作为贯穿整个课程的主线,说明当这个系统到达某一个阶段时,我们会遇到什么问题,然后要采用什么样的方案应对,应对的过程中又涉及哪些技术点。通过这样的讲述方式,力求以案例引出问题,能够让你了解遇到不同问题时,解决思路是怎样的,**当然,在这个过程中,我希望你能多加思考,然后将学到的知识活学活用到实际的项目中。**
**接下来,让我们正式进入课程。**
来想象这样一个场景一天公司CEO把你叫到会议室告诉你公司看到了一个新的商业机会希望你能带领一位兄弟迅速研发出一套面向某个垂直领域的电商系统。
在人手紧张时间不足的情况下为了能够完成任务你毫不犹豫地采用了最简单的架构前端一台Web服务器运行业务代码后端一台数据库服务器存储业务数据。
<img src="https://static001.geekbang.org/resource/image/83/6a/838911dd61e5a61408c3bf96871b846a.jpg" alt="">
这个架构图是我们每个人最熟悉的,最简单的架构原型,很多系统在一开始都是长这样的,只是随着业务复杂度的提高,架构做了叠加,然后看起来就越来越复杂了。
再说回我们的垂直电商系统系统一开始上线之后虽然用户量不大但运行平稳你很有成就感不过CEO觉得用户量太少了所以紧急调动运营同学做了一次全网的流量推广。
这一推广很快带来了一大波流量,**但这时,系统的访问速度开始变慢。**
分析程序的日志之后你发现系统慢的原因出现在和数据库的交互上。因为你们数据库的调用方式是先获取数据库的连接然后依靠这条连接从数据库中查询数据最后关闭连接释放数据库资源。这种调用方式下每次执行SQL都需要重新建立连接所以你怀疑是不是频繁地建立数据库连接耗费时间长导致了访问慢的问题。
**那么为什么频繁创建连接会造成响应时间慢呢?来看一个实际的测试。**
我用"tcpdump -i bond0 -nn -tttt port 4490"命令抓取了线上MySQL建立连接的网络包来做分析从抓包结果来看整个MySQL的连接过程可以分为两部分
**第一部分是前三个数据包。**第一个数据包是客户端向服务端发送的一个“SYN”包第二个包是服务端回给客户端的“ACK”包以及一个“SYN”包第三个包是客户端回给服务端的“ACK”包熟悉TCP协议的同学可以看出这是一个TCP的三次握手过程。
**第二部分是MySQL服务端校验客户端密码的过程。**其中第一个包是服务端发给客户端要求认证的报文第二和第三个包是客户端将加密后的密码发送给服务端的包最后两个包是服务端回给客户端认证OK的报文。从图中你可以看到整个连接过程大概消耗了4ms969012-964904
<img src="https://static001.geekbang.org/resource/image/3d/1b/3d2f10c8fb21873f482688dba6f4f71b.jpg" alt="">
那么单条SQL执行时间是多少呢我们统计了一段时间的SQL执行时间发现SQL的平均执行时间大概是1ms也就是说相比于SQL的执行MySQL建立连接的过程是比较耗时的。这在请求量小的时候其实影响不大因为无论是建立连接还是执行SQL耗时都是毫秒级别的。可是请求量上来之后如果按照原来的方式建立一次连接只执行一条SQL的话1s只能执行200次数据库的查询而数据库建立连接的时间占了其中4/5。
**那这时你要怎么做呢?**
一番谷歌搜索之后你发现解决方案也很简单只要使用连接池将数据库连接预先建立好这样在使用的时候就不需要频繁地创建连接了。调整之后你发现1s就可以执行1000次的数据库查询查询性能大大提升了。
## 用连接池预先建立数据库连接
虽然短时间解决了问题,不过你还是想彻底搞明白解决问题的核心原理,于是又开始补课。
其实在开发过程中我们会用到很多的连接池像是数据库连接池、HTTP连接池、Redis连接池等等。而连接池的管理是连接池设计的核心**我就以数据库连接池为例,来说明一下连接池管理的关键点。**
数据库连接池有两个最重要的配置:**最小连接数和最大连接数,**它们控制着从连接池中获取连接的流程:
- 如果当前连接数小于最小连接数,则创建新的连接处理数据库请求;
- 如果连接池中有空闲连接则复用空闲连接;
- 如果空闲池中没有连接并且当前连接数小于最大连接数,则创建新的连接处理请求;
- 如果当前连接数已经大于等于最大连接数则按照配置中设定的时间C3P0的连接池配置是checkoutTimeout等待旧的连接可用
- 如果等待超过了这个设定时间则向用户抛出错误。
这个流程你不用死记,非常简单。你可以停下来想想如果你是连接池的设计者你会怎么设计,有哪些关键点,这个设计思路在我们以后的架构设计中经常会用到。
为了方便你理解记忆这个流程,我来举个例子。
假设你在机场里经营着一家按摩椅的小店店里一共摆着10台按摩椅类比最大连接数为了节省成本按摩椅费电你平时会保持店里开着4台按摩椅最小连接数其他6台都关着。
有顾客来的时候如果平时保持启动的4台按摩椅有空着的你直接请他去空着的那台就好了。但如果顾客来的时候4台按摩椅都不空着那你就会新启动一台直到你的10台按摩椅都被用完。
那10台按摩椅都被用完之后怎么办呢你会告诉用户稍等一会儿我承诺你5分钟等待时间之内必定能空出来然后第11位用户就开始等着。这时会有两个结果如果5分钟之内有空出来的那顾客直接去空出来的那台按摩椅就可以了但如果用户等了5分钟都没空出来那你就得赔礼道歉让用户去其他店再看看。
对于数据库连接池根据我的经验一般在线上我建议最小连接数控制在10左右最大连接数控制在2030左右即可。
在这里,你需要注意池子中连接的维护问题,也就是我提到的按摩椅。有的按摩椅虽然开着,但有的时候会有故障,一般情况下,“按摩椅故障”的原因可能有以下几种:
1.数据库的域名对应的IP发生了变更池子的连接还是使用旧的IP当旧的IP下的数据库服务关闭后再使用这个连接查询就会发生错误
2.MySQL有个参数是“wait_timeout”控制着当数据库连接闲置多长时间后数据库会主动地关闭这条连接。这个机制对于数据库使用方是无感知的所以当我们使用这个被关闭的连接时就会发生错误。
那么,作为按摩椅店老板,你怎么保证你启动着的按摩椅一定是可用的呢?
1.启动一个线程来定期检测连接池中的连接是否可用比如使用连接发送“select 1”的命令给数据库看是否会抛出异常如果抛出异常则将这个连接从连接池中移除并且尝试关闭。目前C3P0连接池可以采用这种方式来检测连接是否可用**也是我比较推荐的方式。**
2.在获取到连接之后先校验连接是否可用如果可用才会执行SQL语句。比如DBCP连接池的testOnBorrow配置项就是控制是否开启这个验证。这种方式在获取连接时会引入多余的开销**在线上系统中还是尽量不要开启,在测试服务上可以使用。**
至此你彻底搞清楚了连接池的工作原理。可是当你刚想松一口气的时候CEO又提出了一个新的需求。你分析了一下这个需求发现在一个非常重要的接口中你需要访问3次数据库。根据经验判断你觉得这里未来肯定会成为系统瓶颈。
进一步想,你觉得可以创建多个线程来并行处理与数据库之间的交互,这样速度就能快了。不过,因为有了上次数据库的教训,你想到在高并发阶段,频繁创建线程的开销也会很大,于是顺着之前的思路继续想,猜测到了线程池。
## 用线程池预先创建线程
果不其然JDK 1.5中引入的ThreadPoolExecutor就是一种线程池的实现它有两个重要的参数coreThreadCount和maxThreadCount这两个参数控制着线程池的执行过程。它的执行原理类似上面我们说的按摩椅店的模式我这里再给你描述下以加深你的记忆
- 如果线程池中的线程数少于coreThreadCount时处理新的任务时会创建新的线程
- 如果线程数大于coreThreadCount则把任务丢到一个队列里面由当前空闲的线程执行
- 当队列中的任务堆积满了的时候则继续创建线程直到达到maxThreadCount
- 当线程数达到maxTheadCount时还有新的任务提交那么我们就不得不将它们丢弃了。
<img src="https://static001.geekbang.org/resource/image/d4/99/d4f7b06f3c28d88d17b5e2d4b49b6999.jpg" alt="">
这个任务处理流程看似简单,实际上有很多坑,你在使用的时候一定要注意。
**首先,** JDK实现的这个线程池优先把任务放入队列暂存起来而不是创建更多的线程它比较适用于执行CPU密集型的任务也就是需要执行大量CPU运算的任务。这是为什么呢因为执行CPU密集型的任务时CPU比较繁忙因此只需要创建和CPU核数相当的线程就好了多了反而会造成线程上下文切换降低任务执行效率。所以当前线程数超过核心线程数时线程池不会增加线程而是放在队列里等待核心线程空闲下来。
但是我们平时开发的Web系统通常都有大量的IO操作比方说查询数据库、查询缓存等等。任务在执行IO操作的时候CPU就空闲了下来这时如果增加执行任务的线程数而不是把任务暂存在队列中就可以在单位时间内执行更多的任务大大提高了任务执行的吞吐量。所以你看Tomcat使用的线程池就不是JDK原生的线程池而是做了一些改造当线程数超过coreThreadCount之后会优先创建线程直到线程数到达maxThreadCount这样就比较适合于Web系统大量IO操作的场景了你在实际使用过程中也可以参考借鉴。
**其次,**线程池中使用的队列的堆积量也是我们需要监控的重要指标,对于实时性要求比较高的任务来说,这个指标尤为关键。
**我在实际项目中就曾经遇到过任务被丢给线程池之后,长时间都没有被执行的诡异问题。**最初我认为这是代码的Bug导致的后来经过排查发现是因为线程池的coreThreadCount和maxThreadCount设置得比较小导致任务在线程池里面大量的堆积在调大了这两个参数之后问题就解决了。跳出这个坑之后我就把重要线程池的队列任务堆积量作为一个重要的监控指标放到了系统监控大屏上。
**最后,**如果你使用线程池请一定记住不要使用无界队列即没有设置固定大小的队列。也许你会觉得使用了无界队列后任务就永远不会被丢弃只要任务对实时性要求不高反正早晚有消费完的一天。但是大量的任务堆积会占用大量的内存空间一旦内存空间被占满就会频繁地触发Full GC造成服务不可用我之前排查过的一次GC引起的宕机起因就是系统中的一个线程池使用了无界队列。
理解了线程池的关键要点,你在系统里加上了这个特性,至此,系统稳定,你圆满完成了公司给你的研发任务。
这时,你回顾一下这两种技术,会发现它们都有一个**共同点:**它们所管理的对象,无论是连接还是线程,它们的创建过程都比较耗时,也比较消耗系统资源。所以,我们把它们放在一个池子里统一管理起来,以达到提升性能和资源复用的目的。
**这是一种常见的软件设计思想,叫做池化技术,**它的核心思想是空间换时间,期望使用预先创建好的对象来减少频繁创建对象的性能开销,同时还可以对对象进行统一的管理,降低了对象的使用的成本,总之是好处多多。
不过,池化技术也存在一些缺陷,比方说存储池子中的对象肯定需要消耗多余的内存,如果对象没有被频繁使用,就会造成内存上的浪费。再比方说,池子中的对象需要在系统启动的时候就预先创建完成,这在一定程度上增加了系统启动时间。
可这些缺陷相比池化技术的优势来说就比较微不足道了,只要我们确认要使用的对象在创建时确实比较耗时或者消耗资源,并且这些对象也确实会被频繁地创建和销毁,我们就可以使用池化技术来优化。
## 课程小结
本节课,我模拟了研发垂直电商系统最原始的场景,在遇到数据库查询性能下降的问题时,我们使用数据库连接池解决了频繁创建连接带来的性能问题,后面又使用线程池提升了并行查询数据库的性能。
其实,连接池和线程池你并不陌生,不过你可能对它们的原理和使用方式上还存在困惑或者误区,我在面试时,就发现有很多的同学对线程池的基本使用方式都不了解。借用这节课,我想再次强调的重点是:
- 池子的最大值和最小值的设置很重要,初期可以依据经验来设置,后面还是需要根据实际运行情况做调整。
- 池子中的对象需要在使用之前预先初始化完成,这叫做池子的预热,比方说使用线程池时就需要预先初始化所有的核心线程。如果池子未经过预热可能会导致系统重启后产生比较多的慢请求。
- 池化技术核心是一种空间换时间优化方法的实践,所以要关注空间占用情况,避免出现空间过度使用出现内存泄露或者频繁垃圾回收等问题。
## 一课一思
在实际的项目中,你可能会用到其他的池化技术,那么结合今天的内容,你可以和我分享一下在研发过程中,还使用过哪些其它池化技术吗?又因池化技术踩过哪些坑,当时你是怎么解决的?欢迎在留言区和我一起讨论,或者将你的实战经验分享给更多的人。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,116 @@
<audio id="audio" title="08 | 数据库优化方案(一):查询请求增加时,如何做主从分离?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/8d/8312245fd6e27fc6f92c7d5ad4dac98d.mp3"></audio>
你好,我是唐扬。
上节课我们用池化技术解决了数据库连接复用的问题这时你的垂直电商系统虽然整体架构上没有变化但是和数据库交互的过程有了变化在你的Web工程和数据库之间增加了数据库连接池减少了频繁创建连接的成本从上节课的测试来看性能上可以提升80%。现在的架构图如下所示:
<img src="https://static001.geekbang.org/resource/image/26/90/2643e13598139d0964bfc40469bd8390.jpg" alt="">
此时你的数据库还是单机部署依据一些云厂商的Benchmark的结果在4核8G的机器上运行MySQL 5.7时大概可以支撑500的TPS和10000的QPS。这时运营负责人说正在准备双十一活动并且公司层面会继续投入资金在全渠道进行推广这无疑会引发查询量骤然增加的问题。那么今天我们就一起来看看当查询请求增加时应该如何做主从分离来解决问题。
## 主从读写分离
其实,大部分系统的访问模型是读多写少,读写请求量的差距可能达到几个数量级。
这很好理解,刷朋友圈的请求量肯定比发朋友圈的量大,淘宝上一个商品的浏览量也肯定远大于它的下单量。因此,我们优先考虑数据库如何抵抗更高的查询请求,那么首先你需要把读写流量区分开,因为这样才方便针对读流量做单独的扩展,这就是我们所说的主从读写分离。
它其实是个流量分离的问题,就好比道路交通管制一样,一个四车道的大马路划出三个车道给领导外宾通过,另外一个车道给我们使用,优先保证领导先行,就是这个道理。
这个方法本身是一种常规的做法,即使在一个大的项目中,它也是一个应对数据库突发读流量的有效方法。
我目前的项目中就曾出现过前端流量突增导致从库负载过高的问题DBA兄弟会优先做一个从库扩容上去这样对数据库的读流量就会落入到多个从库上从库的负载就降了下来然后研发同学再考虑使用什么样的方案将流量挡在数据库层之上。
## 主从读写的两个技术关键点
一般来说在主从读写分离机制中,我们将一个数据库的数据拷贝为一份或者多份,并且写入到其它的数据库服务器中,原始的数据库我们称为主库,主要负责数据的写入,拷贝的目标数据库称为从库,主要负责支持数据查询。可以看到,主从读写分离有两个技术上的关键点:
1.一个是数据的拷贝,我们称为主从复制;<br>
2.在主从分离的情况下,我们如何屏蔽主从分离带来的访问数据库方式的变化,让开发同学像是在使用单一数据库一样。
接下来,我们分别来看一看。
#### 1. 主从复制
我先以MySQL为例介绍一下主从复制。
MySQL的主从复制是依赖于binlog的也就是记录MySQL上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将binlog中的数据从主库传输到从库上一般这个过程是异步的即主库上的操作不会等待binlog同步的完成。
**主从复制的过程是这样的:**首先从库在连接到主节点时会创建一个IO线程用以请求主库更新的binlog并且把接收到的binlog信息写入一个叫做relay log的日志文件中而主库也会创建一个log dump线程来发送binlog给从库同时从库还会创建一个SQL线程读取relay log中的内容并且在从库中做回放最终实现主从的一致性。这是一种比较常见的主从复制方式。
在这个方案中使用独立的log dump线程是一种异步的方式可以避免对主库的主体更新流程产生影响而从库在接收到信息后并不是写入从库的存储中是写入一个relay log是避免写入从库实际存储会比较耗时最终造成从库和主库延迟变长。
<img src="https://static001.geekbang.org/resource/image/57/4d/575ef1a6dc6463e4c5a60a3752d8554d.jpg" alt="">
你会发现基于性能的考虑主库的写入流程并没有等待主从同步完成就会返回结果那么在极端的情况下比如说主库上binlog还没有来得及刷新到磁盘上就出现了磁盘损坏或者机器掉电就会导致binlog的丢失最终造成主从数据的不一致。**不过,这种情况出现的概率很低,对于互联网的项目来说是可以容忍的。**
做了主从复制之后,我们就可以在写入时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响到读请求的执行。同时呢,在读流量比较大的情况下,我们可以部署多个从库共同承担读流量,这就是所说的“一主多从”部署方式,在你的垂直电商项目中就可以通过这种方式来抵御较高的并发读流量。另外,从库也可以当成一个备库来使用,以避免主库故障导致数据丢失。
**那么你可能会说,是不是我无限制地增加从库的数量就可以抵抗大量的并发呢?**实际上并不是的。因为随着从库数量增加从库连接上来的IO线程比较多主库也需要创建同样多的log dump线程来处理复制的请求对于主库资源消耗比较高同时受限于主库的网络带宽所以在实际使用中一般一个主库最多挂35个从库。
**当然,主从复制也有一些缺陷,**除了带来了部署上的复杂度,还有就是会带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响,我举个例子你就明白了。
在发微博的过程中会有些同步的操作像是更新数据库的操作也有一些异步的操作比如说将微博的信息同步给审核系统所以我们在更新完主库之后会将微博的ID写入消息队列再由队列处理机依据ID在从库中获取微博信息再发送给审核系统。此时如果主从数据库存在延迟会导致在从库中获取不到微博信息整个流程会出现异常。
<img src="https://static001.geekbang.org/resource/image/d0/44/d06716649d3894e8c2b2bf242b1ab544.jpg" alt="">
这个问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,纯粹以上面的例子来说,我就有三种解决方案:
**第一种方案是数据的冗余。**你可以在发送消息队列时不仅仅发送微博ID而是发送队列处理机需要的所有微博信息借此避免从数据库中重新查询数据。
**第二种方案是使用缓存。**我可以在同步写数据库的同时也把微博的数据写入到Memcached缓存里面这样队列处理机在获取微博信息的时候会优先查询缓存这样也可以保证数据的一致性。
**最后一种方案是查询主库。**我可以在队列处理机中不查询从库而改为查询主库。不过,这种方式使用起来要慎重,要明确查询的量级不会很大,是在主库的可承受范围之内,否则会对主库造成比较大的压力。
我会优先考虑第一种方案,因为这种方式足够简单,不过可能造成单条消息比较大,从而增加了消息发送的带宽和时间。
缓存的方案比较适合新增数据的场景在更新数据的场景下先更新缓存可能会造成数据的不一致比方说两个线程同时更新数据线程A把缓存中的数据更新为1此时另一个线程B把缓存中的数据更新为2然后线程B又更新数据库中的数据为2此时线程A更新数据库中的数据为1这样数据库中的值1和缓存中的值2就不一致了。
最后,若非万不得已的情况下,我不会使用第三种方案。原因是这种方案要提供一个查询主库的接口,在团队开发的过程中,你很难保证其他同学不会滥用这个方法,而一旦主库承担了大量的读请求导致崩溃,那么对于整体系统的影响是极大的。
所以对这三种方案来说,你要有所取舍,根据实际项目情况做好选择。
**另外,主从同步的延迟,是我们排查问题时很容易忽略的一个问题。**有时候我们遇到从数据库中获取不到信息的诡异问题时,会纠结于代码中是否有一些逻辑会把之前写入的内容删除,但是你又会发现,过了一段时间再去查询时又可以读到数据了,这基本上就是主从延迟在作怪。所以,一般我们会把从库落后的时间作为一个重点的数据库指标做监控和报警,正常的时间是在毫秒级别,一旦落后的时间达到了秒级别就需要告警了。
#### 2. 如何访问数据库
我们已经使用主从复制的技术将数据复制到了多个节点,也实现了数据库读写的分离,这时,对于数据库的使用方式发生了变化。以前只需要使用一个数据库地址就好了,现在需要使用一个主库地址和多个从库地址,并且需要区分写入操作和查询操作,如果结合下一节课中要讲解的内容“分库分表”,复杂度会提升更多。**为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。**
第一类以淘宝的TDDL Taobao Distributed Data Layer为代表以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理它的配置管理着多个数据源每个数据源对应一个数据库可能是主库可能是从库。当有一个数据库请求时中间件将SQL语句发给某一个指定的数据源来处理然后将处理结果返回。
这一类中间件的优点是简单易用没有多余的部署成本因为它是植入到应用程序内部与应用程序一同运行的所以比较适合运维能力较弱的小团队使用缺点是缺乏多语言的支持目前业界这一类的主流方案除了TDDL还有早期的网易DDB它们都是Java语言开发的无法支持其他的语言。另外版本升级也依赖使用方更新比较困难。
另一类是单独部署的代理层方案这一类方案代表比较多如早期阿里巴巴开源的Cobar基于Cobar开发出来的Mycat360开源的Atlas美团开源的基于Atlas开发的DBProxy等等。
这一类中间件部署在独立的服务器上业务代码如同在使用单一数据库一样使用它实际上它内部管理着很多的数据源当有数据库请求时它会对SQL语句做必要的改写然后发往指定的数据源。
它一般使用标准的MySQL通信协议所以可以很好地支持多语言。由于它是独立部署的所以也比较方便进行维护升级比较适合有一定运维能力的大中型团队使用。它的缺陷是所有的SQL语句都需要跨两次网络从应用到代理层和从代理层到数据源所以在性能上会有一些损耗。
<img src="https://static001.geekbang.org/resource/image/e7/ff/e7e9430cbcb104764529ca5e01e6b3ff.jpg" alt="">
这些中间件,对你而言,可能并不陌生,但是我想让你注意到是,**在使用任何中间件的时候一定要保证对于中间件有足够深入的了解,否则一旦出了问题没法快速地解决就悲剧了。**
**我之前的一个项目中,**一直使用自研的一个组件来实现分库分表后来发现这套组件有一定几率会产生对数据库多余的连接于是团队讨论后决定替换成Sharding-JDBC。原本以为是一次简单的组件切换结果上线后发现两个问题一是因为使用姿势不对会偶发地出现分库分表不生效导致扫描所有库表的情况二是偶发地出现查询延时达到秒级别。由于对Sharding-JDBC没有足够了解这两个问题我们都没有很快解决后来不得已只能切回原来的组件在找到问题之后再进行切换。
## 课程小结
本节课,我带你了解了查询量增加时,我们如何通过主从分离和一主多从部署抵抗增加的数据库流量的,你除了掌握主从复制的技术之外,还需要了解主从分离会带来什么问题以及它们的解决办法。这里我想让你明确的要点主要有:
1.主从读写分离以及部署一主多从可以解决突发的数据库读流量,是一种数据库横向扩展的方法;
2.读写分离后,主从的延迟是一个关键的监控指标,可能会造成写入数据之后立刻读的时候读取不到的情况;
3.业界有很多的方案可以屏蔽主从分离之后数据库访问的细节让开发人员像是访问单一数据库一样包括有像TDDL、Sharding-JDBC这样的嵌入应用内部的方案也有像Mycat这样的独立部署的代理方案。
其实,我们可以把主从复制引申为存储节点之间互相复制存储数据的技术,它可以实现数据的冗余,以达到备份和提升横向扩展能力的作用。在使用主从复制这个技术点时,你一般会考虑两个问题:
1.主从的一致性和写入性能的权衡,如果你要保证所有从节点都写入成功,那么写入性能一定会受影响;如果你只写入主节点就返回成功,那么从节点就有可能出现数据同步失败的情况,从而造成主从不一致,**而在互联网的项目中,我们一般会优先考虑性能而不是数据的强一致性。**
2.主从的延迟问题,很多诡异的读取不到数据的问题都可能会和它有关,如果你遇到这类问题不妨先看看主从延迟的数据。
我们采用的很多组件都会使用到这个技术比如Redis也是通过主从复制实现读写分离Elasticsearch中存储的索引分片也可以被复制到多个节点中写入到HDFS中文件也会被复制到多个DataNode中。只是不同的组件对于复制的一致性、延迟要求不同采用的方案也不同。**但是这种设计的思想是通用的,是你需要了解的,这样你在学习其他存储组件的时候就能够触类旁通了。**
## 一课一思
我们提到,存储节点间互相复制数据是一种常见的,提升系统可用性和性能的方式,那么你还了解哪些组件有使用这种方式呢?它们的复制方式又是如何的呢?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,105 @@
<audio id="audio" title="09 | 数据库优化方案(二):写入数据量增加时,如何实现分库分表?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/8f/5f/8f607b10825ce9a986b165f77febf35f.mp3"></audio>
你好,我是唐扬。
前一节课,我们学习了在高并发下数据库的一种优化方案:读写分离,它就是依靠主从复制的技术使得数据库实现了数据复制为多份,增强了抵抗大量并发读请求的能力,提升了数据库的查询性能的同时,也提升了数据的安全性。当某一个数据库节点,无论是主库还是从库发生故障时,我们还有其他的节点中存储着全量的数据,保证数据不会丢失。此时,你的电商系统的架构图变成了下面这样:
<img src="https://static001.geekbang.org/resource/image/05/23/05fa7f7a861ebedc4d8f0c57bc88b023.jpg" alt="">
这时公司CEO突然传来一个好消息运营推广持续带来了流量你所设计的电商系统的订单量突破了五千万。订单数据都是单表存储的你的压力倍增因为无论是数据库的查询还是写入性能都在下降数据库的磁盘空间也在报警。所以你主动分析现阶段自己需要考虑的问题并寻求高效的解决方式以便系统能正常运转下去。**你考虑的问题主要有以下几点:**
1.系统正在持续不断地发展,注册的用户越来越多,产生的订单越来越多,数据库中存储的数据也越来越多,单个表的数据量超过了千万甚至到了亿级别。这时即使你使用了索引,索引占用的空间也随着数据量的增长而增大,数据库就无法缓存全量的索引信息,那么就需要从磁盘上读取索引数据,就会影响到查询的性能了。**那么这时你要如何提升查询性能呢?**
2.数据量的增加也占据了磁盘的空间,数据库在备份和恢复的时间变长,**你如何让数据库系统支持如此大的数据量呢?**
3.不同模块的数据,比如用户数据和用户关系数据,全都存储在一个主库中,一旦主库发生故障,所有的模块都会受到影响,**那么如何做到不同模块的故障隔离呢?**
4.你已经知道了在4核8G的云服务器上对MySQL 5.7做Benchmark大概可以支撑500TPS和10000QPS你可以看到数据库对于写入性能要弱于数据查询的能力那么随着系统写入请求量的增长**数据库系统如何来处理更高的并发写入请求呢?**
这些问题你可以归纳成,数据库的写入请求量大造成的性能和可用性方面的问题,要解决这些问题,你所采取的措施就是对数据进行分片。这样可以很好地分摊数据库的读写压力,也可以突破单机的存储瓶颈,而常见的一种方式是对数据库做“分库分表”。
分库分表是一个很常见的技术方案,你应该有所了解。那你会说了:“既然这个技术很普遍,而我又有所了解,那你为什么还要提及这个话题呢?”因为以我过往的经验来看,不少人会在“分库分表”这里踩坑,主要体现在:
1.对如何使用正确的分库分表方式一知半解,没有明白使用场景和方法。比如,一些同学会在查询时不使用分区键;
2.分库分表引入了一些问题后,没有找到合适的解决方案。比如,会在查询时使用大量连表查询等等。
本节课,我就带你解决这两个问题,从常人容易踩坑的地方,跳出来。
## 如何对数据库做垂直拆分
分库分表是一种常见的将数据分片的方式,它的基本思想是依照某一种策略将数据尽量平均地分配到多个数据库节点或者多个表中。
不同于主从复制时数据是全量地被拷贝到多个节点,分库分表后,每个节点只保存部分的数据,这样可以有效地减少单个数据库节点和单个数据表中存储的数据量,在解决了数据存储瓶颈的同时也能有效地提升数据查询的性能。同时,因为数据被分配到多个数据库节点上,那么数据的写入请求也从请求单一主库变成了请求多个数据分片节点,在一定程度上也会提升并发写入的性能。
**比如,我之前做过一个直播项目,**在这个项目中,需要存储用户在直播间中发的消息以及直播间中的系统消息,你知道这些消息量极大,有些比较火的直播间有上万条留言是很常见的事儿,日积月累下来就积攒了几亿的数据,查询的性能和存储空间都扛不住了。没办法,就只能加班加点重构,启动多个数据库来分摊写入压力和容量的压力,也需要将原来单库的数据迁移到新启动的数据库节点上,好在最后成功完成分库分表和数据迁移校验工作,不过也着实花费了不少的时间和精力。
数据库分库分表的方式有两种:一种是垂直拆分,另一种是水平拆分。这两种方式,在我看来,掌握拆分方式是关键,理解拆分原理是内核。所以你在学习时,最好可以结合自身业务来思考。
垂直拆分,顾名思义就是对数据库竖着拆分,也就是将数据库的表拆分到多个不同的数据库中。
垂直拆分的原则一般是按照业务类型来拆分核心思想是专库专用将业务耦合度比较高的表拆分到单独的库中。举个形象的例子就是在整理衣服的时候将羽绒服、毛衣、T恤分别放在不同的格子里。这样可以解决我在开篇提到的第三个问题把不同的业务的数据分拆到不同的数据库节点上这样一旦数据库发生故障时只会影响到某一个模块的功能不会影响到整体功能从而实现了数据层面的故障隔离。
**我还是以微博系统为例来给你说明一下。**
在微博系统中有和用户相关的表,有和内容相关的表,有和关系相关的表,这些表都存储在主库中。在拆分后,我们期望用户相关的表分拆到用户库中,内容相关的表分拆到内容库中,关系相关的表分拆到关系库中。
<img src="https://static001.geekbang.org/resource/image/77/40/7774c9393a6295b2d5e0f1a9fa7a5940.jpg" alt="">
对数据库进行垂直拆分是一种偏常规的方式,这种方式其实你会比较常用,不过拆分之后,虽然可以暂时缓解存储容量的瓶颈,但并不是万事大吉,因为数据库垂直拆分后依然不能解决某一个业务模块的数据大量膨胀的问题,一旦你的系统遭遇某一个业务库的数据量暴增,在这个情况下,你还需要继续寻找可以弥补的方式。
比如微博关系量早已经过了千亿,单一的数据库或者数据表已经远远不能满足存储和查询的需求了,这个时候,你需要将数据拆分到多个数据库和数据表中,**也就是对数据库和数据表做水平拆分了。**
## 如何对数据库做水平拆分
和垂直拆分的关注点不同,垂直拆分的关注点在于业务相关性,而水平拆分指的是将单一数据表按照某一种规则拆分到多个数据库和多个数据表中,关注点在数据的特点。
**拆分的规则有下面这两种:**
1.按照某一个字段的哈希值做拆分这种拆分规则比较适用于实体表比如说用户表内容表我们一般按照这些实体表的ID字段来拆分。比如说我们想把用户表拆分成16个库每个库是64张表那么可以先对用户ID做哈希哈希的目的是将ID尽量打散然后再对16取余这样就得到了分库后的索引值对64取余就得到了分表后的索引值。
<img src="https://static001.geekbang.org/resource/image/7c/39/7c6af43da41bb197be753207d4b9e039.jpg" alt="">
2.另一种比较常用的是按照某一个字段的区间来拆分,比较常用的是时间字段。你知道在内容表里面有“创建时间”的字段,而我们也是按照时间来查看一个人发布的内容。我们可能会要看昨天的内容,也可能会看一个月前发布的内容,这时就可以按照创建时间的区间来分库分表,比如说可以把一个月的数据放入一张表中,这样在查询时就可以根据创建时间先定位数据存储在哪个表里面,再按照查询条件来查询。
**一般来说,列表数据可以使用这种拆分方式,**比如一个人一段时间的订单一段时间发布的内容。但是这种方式可能会存在明显的热点这很好理解嘛你当然会更关注最近我买了什么发了什么所以查询的QPS也会更多一些对性能有一定的影响。另外使用这种拆分规则后数据表要提前建立好否则如果时间到了2020年元旦DBADatabase Administrator数据库管理员却忘记了建表那么2020年的数据就没有库表可写了就会发生故障了。
<img src="https://static001.geekbang.org/resource/image/40/c4/40ec1287d871d656f508d5e108f675c4.jpg" alt="">
数据库在分库分表之后,数据的访问方式也有了极大的改变,原先只需要根据查询条件到从库中查询数据即可,现在则需要先确认数据在哪一个库表中,再到那个库表中查询数据。这种复杂度也可以通过数据库中间件来解决,我们在[08讲](https://time.geekbang.org/column/article/145095)中已经有所讲解,这里就不再赘述了,不过,我想再次强调的是,你需要对所使用数据库中间件的原理有足够的了解,和足够强的运维上的把控能力。
不过,你要知道的是,分库分表虽然能够解决数据库扩展性的问题,但是它也给我们的使用带来了一些问题。
## 解决分库分表引入的问题
分库分表引入的一个最大的问题就是**引入了分库分表键,也叫做分区键,**也就是我们对数据库做分库分表所依据的字段。
从分库分表规则中你可以看到无论是哈希拆分还是区间段的拆分我们首先都需要选取一个数据库字段这带来一个问题是我们之后所有的查询都需要带上这个字段才能找到数据所在的库和表否则就只能向所有的数据库和数据表发送查询命令。如果像上面说的要拆分成16个库和64张表那么一次数据的查询会变成16*64=1024次查询查询的性能肯定是极差的。
**当然,方法总比问题多,**针对这个问题我们也会有一些相应的解决思路。比如在用户库中我们使用ID作为分区键这时如果需要按照昵称来查询用户时你可以按照昵称作为分区键再做一次拆分但是这样会极大地增加存储成本如果以后我们还需要按照注册时间来查询时要怎么办呢再做一次拆分吗
**所以最合适的思路是**你要建立一个昵称和ID的映射表在查询的时候要先通过昵称查询到ID再通过ID查询完整的数据这个表也可以是分库分表的也需要占用一定的存储空间但是因为表中只有两个字段所以相比重新做一次拆分还是会节省不少的空间的。
**分库分表引入的另外一个问题是一些数据库的特性在实现时可能变得很困难。**比如说多表的JOIN在单库时是可以通过一个SQL语句完成的但是拆分到多个数据库之后就无法跨库执行SQL了不过好在我们对于JOIN的需求不高即使有也一般是把两个表的数据取出后在业务代码里面做筛选复杂是有一些不过是可以实现的。再比如说在未分库分表之前查询数据总数时只需要在SQL中执行count()即可现在数据被分散到多个库表中我们可能要考虑其他的方案比方说将计数的数据单独存储在一张表中或者记录在Redis里面。
当然,虽然分库分表会对我们使用数据库带来一些不便,但是相比它所带来的扩展性和性能方面的提升,我们还是需要做的,因为,经历过分库分表后的系统,才能够突破单机的容量和请求量的瓶颈,就比如说,我在开篇提到的我们的电商系统,它正是经历了分库分表,才会解决订单表数据量过大带来的性能衰减和容量瓶颈。
## 课程小结
总的来说,在面对数据库容量瓶颈和写并发量大的问题时,你可以采用垂直拆分和水平拆分来解决,不过你要注意,这两种方式虽然能够解决问题,但是也会引入诸如查询数据必须带上分区键,列表总数需要单独冗余存储等问题。
而且,你需要了解的是在实现分库分表过程中,数据从单库单表迁移多库多表是一件既繁杂又容易出错的事情,而且如果我们初期没有规划得当,后面要继续增加数据库数或者表数时,我们还要经历这个迁移的过程。**所以,从我的经验出发,对于分库分表的原则主要有以下几点:**
1.如果在性能上没有瓶颈点那么就尽量不做分库分表;
2.如果要做就尽量一次到位比如说16库每个库64表就基本能够满足几年内你的业务的需求。
3.很多的NoSQL数据库例如HbaseMongoDB都提供auto sharding的特性如果你的团队内部对于这些组件比较熟悉有较强的运维能力那么也可以考虑使用这些NoSQL数据库替代传统的关系型数据库。
其实,在我看来,有很多人并没有真正从根本上搞懂为什么要拆分,拆分后会带来哪些问题,只是一味地学习大厂现有的拆分方法,从而导致问题频出。**所以,你在使用一个方案解决一个问题的时候一定要弄清楚原理,搞清楚这个方案会带来什么问题,要如何来解决,要知其然也知其所以然,这样才能在解决问题的同时避免踩坑。**
## 一课一思
分库分表实际上是分布式存储中一种数据分片的解决方案,那么你还了解哪些分布式存储组件也使用了类似的技术呢?它的实现方式你了解吗?欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,103 @@
<audio id="audio" title="10 | 发号器如何保证分库分表后ID的全局唯一性" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/83/a7/83c6299503a37695b0592ba5caba6da7.mp3"></audio>
你好,我是唐扬。
在前面两节课程中,我带你了解了分布式存储两个核心问题:数据冗余和数据分片,以及在传统关系型数据库中是如何解决的。当我们面临高并发的查询数据请求时,可以使用主从读写分离的方式,部署多个从库分摊读压力;当存储的数据量达到瓶颈时,我们可以将数据分片存储在多个节点上,降低单个存储节点的存储压力,此时我们的架构变成了下面这个样子:
<img src="https://static001.geekbang.org/resource/image/14/f5/14dc3467723db359347551c24819c3f5.jpg" alt="">
你可以看到我们通过分库分表和主从读写分离的方式解决了数据库的扩展性问题但是在09讲我也提到过数据库在分库分表之后我们在使用数据库时存在的许多限制比方说查询的时候必须带着分区键一些聚合类的查询像是count())性能较差,需要考虑使用计数器等其它的解决方案,其实分库分表还有一个问题我在[09讲](https://time.geekbang.org/column/article/145480)中没有提到,就是主键的全局唯一性的问题。本节课,我将带你一起来了解,在分库分表后如何生成全局唯一的数据库主键。
不过,在探究这个问题之前,你需要对“使用什么字段作为主键”这个问题有所了解,这样才能为我们后续探究如何生成全局唯一的主键做好铺垫。
## 数据库的主键要如何选择?
数据库中的每一条记录都需要有一个唯一的标识,依据数据库的第二范式,数据库中每一个表中都需要有一个唯一的主键,其他数据元素和主键一一对应。
**那么关于主键的选择就成为一个关键点了,**一般来讲,你有两种选择方式:
1.使用业务字段作为主键比如说对于用户表来说可以使用手机号email或者身份证号作为主键。
2.使用生成的唯一ID作为主键。
不过对于大部分场景来说第一种选择并不适用比如像评论表你就很难找到一个业务字段作为主键因为在评论表中你很难找到一个字段唯一标识一条评论。而对于用户表来说我们需要考虑的是作为主键的业务字段是否能够唯一标识一个人一个人可以有多个email和手机号一旦出现变更email或者手机号的情况就需要变更所有引用的外键信息所以使用email或者手机作为主键是不合适的。
身份证号码确实是用户的唯一标识但是由于它的隐私属性并不是一个用户系统的必须属性你想想你的系统如果没有要求做实名认证那么肯定不会要求用户填写身份证号码的。并且已有的身份证号码是会变更的比如在1999年时身份证号码就从15位变更为18位但是主键一旦变更以这个主键为外键的表也都要随之变更这个工作量是巨大的。
**因此我更倾向于使用生成的ID作为数据库的主键。**不单单是因为它的唯一性,更是因为一旦生成就不会变更,可以随意引用。
在单库单表的场景下我们可以使用数据库的自增字段作为ID因为这样最简单对于开发人员来说也是透明的。但是当数据库分库分表后使用自增字段就无法保证ID的全局唯一性了。
想象一下当我们分库分表之后同一个逻辑表的数据被分布到多个库中这时如果使用数据库自增字段作为主键那么只能保证在这个库中是唯一的无法保证全局的唯一性。那么假如你来设计用户系统的时候使用自增ID作为用户ID就可能出现两个用户有两个相同的ID这是不可接受的那么你要怎么做呢我建议你搭建发号器服务来生成全局唯一的ID。
## 基于Snowflake算法搭建发号器
从我历年所经历的项目中我主要使用的是变种的Snowflake算法来生成业务需要的ID的本讲的重点也是运用它去解决ID全局唯一性的问题。搞懂这个算法知道它是怎么实现的就足够你应用它来设计一套分布式发号器了不过你可能会说了“那你提全局唯一性怎么不提UUID呢
没错UUIDUniversally Unique Identifier通用唯一标识码不依赖于任何第三方系统所以在性能和可用性上都比较好我一般会使用它生成Request ID来标记单次请求但是如果用它来作为数据库主键它会存在以下几点问题。
首先生成的ID最好具有单调递增性也就是有序的而UUID不具备这个特点。为什么ID要是有序的呢**因为在系统设计时ID有可能成为排序的字段。**我给你举个例子。
比如你要实现一套评论的系统时你一般会设计两个表一张评论表存储评论的详细信息其中有ID字段有评论的内容还有评论人ID被评论内容的ID等等以ID字段作为分区键另一个是评论列表存储着内容ID和评论ID的对应关系以内容ID为分区键。
我们在获取内容的评论列表时需要按照时间倒序排列因为ID是时间上有序的所以我们就可以按照评论ID的倒序排列。而如果评论ID不是在时间上有序的话我们就需要在评论列表中再存储一个多余的创建时间的列用作排序假设内容ID、评论ID和时间都是使用8字节存储我们就要多出50%的存储空间存储时间字段,造成了存储空间上的浪费。
**另一个原因在于ID有序也会提升数据的写入性能。**
我们知道MySQL InnoDB存储引擎使用B+树存储索引数据而主键也是一种索引。索引数据在B+树中是有序排列的就像下面这张图一样图中21026都是记录的ID也是索引数据。
<img src="https://static001.geekbang.org/resource/image/83/71/83e43a3868c076fccdc633f5ec2b0171.jpg" alt="">
这时当插入的下一条记录的ID是递增的时候比如插入30时数据库只需要把它追加到后面就好了。但是如果插入的数据是无序的比如ID是13那么数据库就要查找13应该插入的位置再挪动13后面的数据这就造成了多余的数据移动的开销。
<img src="https://static001.geekbang.org/resource/image/34/2a/34b2a05a6fc70730748eaaed12bc9b2a.jpg" alt="">
我们知道机械磁盘在完成随机的写时,需要先做“寻道”找到要写入的位置,也就是让磁头找到对应的磁道,这个过程是非常耗时的。而顺序写就不需要寻道,会大大提升索引的写入性能。
**UUID不能作为ID的另一个原因是它不具备业务含义**其实现实世界中使用的ID中都包含有一些有意义的数据这些数据会出现在ID的固定的位置上。比如说我们使用的身份证的前六位是地区编号714位是身份证持有人的生日不同城市电话号码的区号是不同的你从手机号码的前三位就可以看出这个手机号隶属于哪一个运营商。而如果生成的ID可以被反解那么从反解出来的信息中我们可以对ID来做验证我们可以从中知道这个ID的生成时间从哪个机房的发号器中生成的为哪个业务服务的对于问题的排查有一定的帮助。
最后UUID是由32个16进制数字组成的字符串如果作为数据库主键使用比较耗费空间。
你能看到UUID方案有很大的局限性也是我不建议你用它的原因而twitter提出的Snowflake算法完全可以弥补UUID存在的不足因为它不仅算法简单易实现也满足ID所需要的全局唯一性单调递增性还包含一定的业务上的意义。
Snowflake的核心思想是将64bit的二进制数字分成若干部分每一部分都存储有特定含义的数据比如说时间戳、机器ID、序列号等等最终生成全局唯一的有序ID。它的标准算法是这样的
<img src="https://static001.geekbang.org/resource/image/2d/8d/2dee7e8e227a339f8f3cb6e7b47c0c8d.jpg" alt="">
从上面这张图中我们可以看到41位的时间戳大概可以支撑pow(2,41)/1000/60/60/24/365年约等于69年对于一个系统是足够了。
如果你的系统部署在多个机房那么10位的机器ID可以继续划分为23位的IDC标示可以支撑4个或者8个IDC机房和78位的机器ID支持128-256台机器12位的序列号代表着每个节点每毫秒最多可以生成4096的ID。
不同公司也会依据自身业务的特点对Snowflake算法做一些改造比如说减少序列号的位数增加机器ID的位数以支持单IDC更多的机器也可以在其中加入业务ID字段来区分不同的业务。**比方说我现在使用的发号器的组成规则就是:**1位兼容位恒为0 + 41位时间信息 + 6位IDC信息支持64个IDC+ 6位业务信息支持64个业务+ 10位自增信息每毫秒支持1024个号
我选择这个组成规则主要是因为我在单机房只部署一个发号器的节点并且使用KeepAlive保证可用性。业务信息指的是项目中哪个业务模块使用比如用户模块生成的ID内容模块生成的ID把它加入进来一是希望不同业务发出来的ID可以不同二是因为在出现问题时可以反解ID知道是哪一个业务发出来的ID。
那么了解了Snowflake算法的原理之后我们如何把它工程化来为业务生成全局唯一的ID呢**一般来说我们会有两种算法的实现方式:**
**一种是嵌入到业务代码里,也就是分布在业务服务器中。**这种方案的好处是业务代码在使用的时候不需要跨网络调用性能上会好一些但是就需要更多的机器ID位数来支持更多的业务服务器。另外由于业务服务器的数量很多我们很难保证机器ID的唯一性所以就需要引入ZooKeeper等分布式一致性组件来保证每次机器重启时都能获得唯一的机器ID。
**另外一个部署方式是作为独立的服务部署,这也就是我们常说的发号器服务。**业务在使用发号器的时候就需要多一次的网络调用但是内网的调用对于性能的损耗有限却可以减少机器ID的位数如果发号器以主备方式部署同时运行的只有一个发号器那么机器ID可以省略这样可以留更多的位数给最后的自增信息位。即使需要机器ID因为发号器部署实例数有限那么就可以把机器ID写在发号器的配置文件里这样可以保证机器ID唯一性也无需引入第三方组件了。**微博和美图都是使用独立服务的方式来部署发号器的性能上单实例单CPU可以达到两万每秒。**
Snowflake算法设计得非常简单且巧妙性能上也足够高效同时也能够生成具有全局唯一性、单调递增性和有业务含义的ID但是它也有一些缺点其中最大的缺点就是它依赖于系统的时间戳一旦系统时间不准就有可能生成重复的ID。所以如果我们发现系统时钟不准就可以让发号器暂时拒绝发号直到时钟准确为止。
另外如果请求发号器的QPS不高比如说发号器每毫秒只发一个ID就会造成生成ID的末位永远是1那么在分库分表时如果使用ID作为分区键就会造成库表分配的不均匀。**这一点,也是我在实际项目中踩过的坑,而解决办法主要有两个:**
1.时间戳不记录毫秒而是记录秒,这样在一个时间区间里可以多发出几个号,避免出现分库分表时数据分配不均。
2.生成的序列号的起始号可以做一下随机这一秒是21下一秒是30这样就会尽量地均衡了。
我在开头提到自己的实际项目中采用的是变种的Snowflake算法也就是说对Snowflake算法进行了一定的改造从上面的内容中你可以看出这些改造一是要让算法中的ID生成规则符合自己业务的特点二是为了解决诸如时间回拨等问题。
其实大厂除了采取Snowflake算法之外还会选用一些其他的方案比如滴滴和美团都有提出基于数据库生成ID的方案。这些方法根植于公司的业务同样能解决分布式环境下ID全局唯一性的问题。对你而言可以多角度了解不同的方法这样能够寻找到更适合自己业务目前场景的解决方案不过我想说的是**方案不在多,而在精,方案没有最好,只有最适合,真正弄懂方法背后的原理,并将它落地,才是你最佳的选择。**
## 课程小结
本节课我结合自己的项目经历带你了解了如何使用Snowflake算法解决分库分表后数据库ID的全局唯一的问题在这个问题中又延伸性地带你了解了生成的ID需要满足单调递增性以及要具有一定业务含义的特性。当然我们重点的内容是讲解如何将Snowflake算法落地以及在落地过程中遇到了哪些坑带你去解决它。
Snowflake的算法并不复杂你在使用的时候可以b不考虑独立部署的问题先想清楚按照自身的业务场景需要如何设计Snowflake算法中的每一部分占的二进制位数。比如你的业务会部署几个IDC应用服务器要部署多少台机器每秒钟发号个数的要求是多少等等然后在业务代码中实现一个简单的版本先使用等到应用服务器数量达到一定规模再考虑独立部署的问题就可以了。这样可以避免多维护一套发号器服务减少了运维上的复杂度。
## 一课一思
今天的课程中我们了解了分布式发号器的实现原理和生成ID的特性那么在你的系统中你的ID是如何生成的呢欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。

View File

@@ -0,0 +1,140 @@
<audio id="audio" title="11 | NoSQL在高并发场景下数据库和NoSQL如何做到互补" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/95/c7/95b00be88903a158151d6091f63f3ec7.mp3"></audio>
你好,我是唐扬。
前几节课,我带你了解了在你的垂直电商项目中,如何将传统的关系型数据库改造成分布式存储服务,以抵抗高并发和大流量的冲击。
对于存储服务来说,我们一般会从两个方面对它做改造:
1.提升它的读写性能尤其是读性能因为我们面对的多是一些读多写少的产品。比方说你离不开的微信朋友圈、微博和淘宝都是查询QPS远远大于写入QPS。
2.增强它在存储上的扩展能力,从而应对大数据量的存储需求。
我之前带你学习的读写分离和分库分表就是从这两方面出发,改造传统的关系型数据库的,但仍有一些问题无法解决。
比如在微博项目中关系的数据量达到了千亿那么即使分隔成1024个库表每张表的数据量也达到了亿级别并且关系的数据量还在以极快的速度增加即使你分隔成再多的库表数据量也会很快增加到瓶颈。这个问题用传统数据库很难根本解决因为它在扩展性方面是很弱的这时就可以利用NoSQL因为它有着天生分布式的能力能够提供优秀的读写性能可以很好地补充传统关系型数据库的短板。那么它是如何做到的呢
这节课我就还是以你的垂直电商系统为例带你掌握如何用NoSQL数据库和关系型数据库互补共同承担高并发和大流量的冲击。
首先我们先来了解一下NoSQL数据库。
## NoSQLNo SQL
NoSQL想必你很熟悉它指的是不同于传统的关系型数据库的其他数据库系统的统称它不使用SQL作为查询语言提供优秀的横向扩展能力和读写性能非常契合互联网项目高并发大数据的特点。所以一些大厂比如小米、微博、陌陌都很倾向使用它来作为高并发大容量的数据存储服务。
NoSQL数据库发展到现在十几年间出现了多种类型我来给你举几个例子
<li>
Redis、LevelDB这样的KV存储。这类存储相比于传统的数据库的优势是极高的读写性能一般对性能有比较高的要求的场景会使用。
</li>
<li>
Hbase、Cassandra这样的列式存储数据库。这种数据库的特点是数据不像传统数据库以行为单位来存储而是以列来存储适用于一些离线数据统计的场景。
</li>
<li>
像MongoDB、CouchDB这样的文档型数据库。这种数据库的特点是Schema Free模式自由数据表中的字段可以任意扩展比如说电商系统中的商品有非常多的字段并且不同品类的商品的字段也都不尽相同使用关系型数据库就需要不断增加字段支持而用文档型数据库就简单很多了。
</li>
在NoSQL数据库刚刚被应用时它被认为是可以替代关系型数据库的银弹在我看来也许因为以下几个方面的原因
- 弥补了传统数据库在性能方面的不足;
- 数据库变更方便,不需要更改原先的数据结构;
- 适合互联网项目常见的大数据量的场景;
不过这种看法是个误区因为慢慢地我们发现在业务开发的场景下还是需要利用SQL语句的强大的查询功能以及传统数据库事务和灵活的索引等功能NoSQL只能作为一些场景的补充。
那么接下来,我就带你了解**NoSQL数据库是如何做到与关系数据库互补的。**了解这部分内容你可以在实际项目中更好地使用NoSQL数据库补充传统数据库的不足。
首先,我们来关注一下数据库的写入性能。
## 使用NoSQL提升写入性能
数据库系统大多使用的是传统的机械磁盘对于机械磁盘的访问方式有两种一种是随机IO另一种是顺序IO。随机IO就需要花费时间做昂贵的磁盘寻道一般来说它的读写效率要比顺序IO小两到三个数量级所以我们想要提升写入的性能就要尽量减少随机IO。
以MySQL的InnoDB存储引擎来说更新binlog、redolog、undolog都是在做顺序IO而更新datafile和索引文件则是在做随机IO而为了减少随机IO的发生关系数据库已经做了很多的优化比如说写入时先写入内存然后批量刷新到磁盘上但是随机IO还是会发生。
索引在InnoDB引擎中是以B+树([上一节课](https://time.geekbang.org/column/article/146454)提到了B+树你可以回顾一下方式来组织的而MySQL主键是聚簇索引一种索引类型数据与索引数据放在一起既然数据和索引数据放在一起那么在数据插入或者更新的时候我们需要找到要插入的位置再把数据写到特定的位置上这就产生了随机的IO。而且一旦发生了页分裂就不可避免会做数据的移动也会极大地损耗写入性能。
**NoSQL数据库是怎么解决这个问题的呢**
它们有多种的解决方式这里我给你讲一种最常见的方案就是很多NoSQL数据库都在使用的**基于LSM树的存储引擎**这种算法使用最多,所以在这里着重剖析一下。
LSM树Log-Structured Merge Tree牺牲了一定的读性能来换取写入数据的高性能Hbase、Cassandra、LevelDB都是用这种算法作为存储的引擎。
它的思想很简单数据首先会写入到一个叫做MemTable的内存结构中在MemTable中数据是按照写入的Key来排序的。为了防止MemTable里面的数据因为机器掉电或者重启而丢失一般会通过写Write Ahead Log的方式将数据备份在磁盘上。
MemTable在累积到一定规模时它会被刷新生成一个新的文件我们把这个文件叫做SSTableSorted String Table。当SSTable达到一定数量时我们会将这些SSTable合并减少文件的数量因为SSTable都是有序的所以合并的速度也很快。
当从LSM树里面读数据时我们首先从MemTable中查找数据如果数据没有找到再从SSTable中查找数据。因为存储的数据都是有序的所以查找的效率是很高的只是因为数据被拆分成多个SSTable所以读取的效率会低于B+树索引。
<img src="https://static001.geekbang.org/resource/image/b4/eb/b4c9c93f22edae091740fa1606d109eb.jpg" alt="">
和LSM树类似的算法有很多比如说TokuDB使用的名为Fractal tree的索引结构它们的核心思想就是将随机IO变成顺序的IO从而提升写入的性能。
在后面的缓存篇中我也将给你着重介绍我们是如何使用KV型NoSQL存储来提升读性能的。所以你看NoSQL数据库补充关系型数据库的第一种方式就是提升读写性能。
## 场景补充
除了可以提升性能之外NoSQL数据库还可以在某些场景下作为传统关系型数据库的补充来看一个具体的例子。
假设某一天CEO找到你并且告诉你他正在为你的垂直电商项目规划搜索的功能需要支持按照商品的名称模糊搜索到对应的商品希望你尽快调研出解决方案。
一开始你认为这非常的简单不就是在数据库里面执行一条类似“select * from product where name like %***%’”的语句吗?可是在实际执行的过程中,却发现了问题。
你发现这类语句并不是都能使用到索引只有后模糊匹配的语句才能使用索引。比如语句“select * from product where name like %电冰箱”就没有使用到字段“name”上的索引而“select * from product where name like ‘索尼%”就使用了“name”上的索引。而一旦没有使用索引就会扫描全表的数据在性能上是无法接受的。
于是你在谷歌上搜索了一下解决方案发现大家都在使用开源组件Elasticsearch来支持搜索的请求它本身是基于“倒排索引”来实现的**那么什么是倒排索引呢?**
倒排索引是指将记录中的某些列做分词然后形成的分词与记录ID之间的映射关系。比如说你的垂直电商项目里面有以下记录
<img src="https://static001.geekbang.org/resource/image/20/57/201ffbb6da51e04894d8dee7eaeb5d57.jpg" alt="">
那么我们将商品名称做简单的分词然后建立起分词和商品ID的对应关系就像下面展示的这样
<img src="https://static001.geekbang.org/resource/image/c9/2f/c919944bcdfd1f1ce576790fc496a62f.jpg" alt="">
这样如果用户搜索电冰箱就可以给他展示商品ID为1和3的两件商品了。
而Elasticsearch作为一种常见的NoSQL数据库**就以倒排索引作为核心技术原理为你提供了分布式的全文搜索服务这在传统的关系型数据库中使用SQL语句是很难实现的。**所以你看NoSQL可以在某些业务场景下代替传统数据库提供数据存储服务。
## 提升扩展性
另外在扩展性方面很多NoSQL数据库也有着先天的优势。还是以你的垂直电商系统为例你已经为你的电商系统增加了评论系统开始你的评估比较乐观觉得电商系统的评论量级不会增长很快所以就为它分了8个库每个库拆分成16张表。
但是评论系统上线之后,存储量级增长的异常迅猛,你不得不将数据库拆分成更多的库表,而数据也要重新迁移到新的库表中,过程非常痛苦,而且数据迁移的过程也非常容易出错。
这时你考虑是否可以考虑使用NoSQL数据库来彻底解决扩展性的问题经过调研你发现它们在设计之初就考虑到了分布式和大数据存储的场景**比如像MongoDB就有三个扩展性方面的特性。**
<li>
其一是Replica也叫做副本集你可以理解为主从分离也就是通过将数据拷贝成多份来保证当主挂掉后数据不会丢失。同时呢Replica还可以分担读请求。Replica中有主节点来承担写请求并且把数据变动记录到oplog里类似于binlog从节点接收到oplog后就会修改自身的数据以保持和主节点的一致。一旦主节点挂掉MongoDB会从从节点中选取一个节点成为主节点可以继续提供写数据服务。
</li>
<li>
其二是Shard也叫做分片你可以理解为分库分表即将数据按照某种规则拆分成多份存储在不同的机器上。MongoDB的Sharding特性一般需要三个角色来支持一个是Shard Server它是实际存储数据的节点是一个独立的Mongod进程二是Config Server也是一组Mongod进程主要存储一些元信息比如说哪些分片存储了哪些数据等最后是Route Server它不实际存储数据仅仅作为路由使用它从Config Server中获取元信息后将请求路由到正确的Shard Server中。
</li>
<img src="https://static001.geekbang.org/resource/image/e8/80/e8cb47c8cc556fce058f7c5cf06d4780.jpg" alt="">
- 其三是负载均衡就是当MongoDB发现Shard之间数据分布不均匀会启动Balancer进程对数据做重新的分配最终让不同Shard Server的数据可以尽量的均衡。当我们的Shard Server存储空间不足需要扩容时数据会自动被移动到新的Shard Server上减少了数据迁移和验证的成本。
你可以看到NoSQL数据库中内置的扩展性方面的特性可以让我们不再需要对数据库做分库分表和主从分离也是对传统数据库一个良好的补充。
你可能会觉得NoSQL已经成熟到可以代替关系型数据库了但是就目前来看NoSQL只能作为传统关系型数据库的补充而存在弥补关系型数据库在性能、扩展性和某些场景下的不足所以你在使用或者选择时要结合自身的场景灵活地运用。
## 课程小结
本节课我带你了解了NoSQL数据库在性能、扩展性上的优势以及它的一些特殊功能特性主要有以下几点
1.在性能方面NoSQL数据库使用一些算法将对磁盘的随机写转换成顺序写提升了写的性能
2.在某些场景下比如全文搜索功能关系型数据库并不能高效地支持需要NoSQL数据库的支持
3.在扩展性方面NoSQL数据库天生支持分布式支持数据冗余和数据分片的特性。
这些都让它成为传统关系型数据库的良好的补充,你需要了解的是,**NoSQL可供选型的种类很多每一个组件都有各自的特点。你在做选型的时候需要对它的实现原理有比较深入的了解最好在运维方面对它有一定的熟悉这样在出现问题时才能及时找到解决方案。**否则盲目跟从地上了一个新的NoSQL数据库最终可能导致会出了故障无法解决反而成为整体系统的拖累。
我在之前的项目中曾经使用Elasticsearch作为持久存储支撑社区的feed流功能初期开发的时候确实很爽你可以针对feed中的任何字段做灵活高效地查询业务功能迭代迅速代码也简单易懂。可是到了后期流量上来之后由于缺少对于Elasticsearch成熟的运维能力造成故障频出尤其到了高峰期就会出现节点不可用的问题而由于业务上的巨大压力又无法分出人力和精力对Elasticsearch深入的学习和了解最后不得不做大的改造切回熟悉的MySQL。**所以对于开源组件的使用不能只停留在只会“hello world”的阶段而应该对它有足够的运维上的把控能力。**
## 一课一思
NoSQL数据库是可以与传统的关系型数据库配合一起解决数据存储问题的那么在日常工作中你用到了哪些NoSQL数据库呢在选型的时候是基于什么样的考虑呢欢迎在留言区与我分享你的经验。
最后,感谢你的阅读,如果这篇文章让你有所收获,也欢迎你将它分享给更多的朋友。