架构师之路总结

通用设计与方法论

一些名词解释
1)nginx:一个高性能的web-server和实施反向代理的软件
2)lvs:Linux Virtual Server,使用集群技术,实现在linux操作系统层面的一个高性能、高可用、负载均衡服务器
3)keepalived:一款用来检测服务状态存活性的软件,常用来做高可用
4)f5:一个高性能、高可用、负载均衡的硬件设备(听上去和lvs功能差不多?)
5)DNS轮询:通过在DNS-server上对一个域名设置多个ip解析,来扩充web-server性能及实施负载均衡的技术

秒杀系统优化思路

  1. 在前端只允许用户提交一次。
  2. 后端只允许 一个登录用户只允许提交一次。
  3. 使用请求队列,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”。

以下是原文写的
      浏览器和APP:做限速
      站点层:按照uid做限速,做页面缓存
      服务层:按照业务做写请求队列控制流量,做数据缓存

秒杀业务逻辑(库存检查,库存冻结,余额检查,余额冻结,订单生成,余额扣减,库存扣减,生成流水,余额解冻,库存解冻)

分布式ID生成方法

网上有许多类似的博客讲解,具体选哪种方式,可根据自己的实际情况。

方法一:使用数据库的 auto_increment 来生成全局唯一递增ID

  • 多个写库时,每个写库设置不同的auto_increment初始值,以及相同的增长步长,以保证每个数据库生成的ID是不同的(上图中库0生成0,3,6,9…,库1生成1,4,7,10,库2生成2,5,8,11…)

方法二:单点批量ID生成服务

  • 数据库写压力大,是因为每次生成ID都访问了数据库,可以使用自己维护主键批量生成ID的方式降低数据库写压力。
  • 如果自动生成主键的服务成了性能瓶颈,则可以使用影子服务(即用一个备份的ID生成服务),这样在主键生成服务挂了重启的时候作为替代服务,这样就可以避免主键出现不连续的问题。

方法三:使用UUID作为主键

  • UUID是字符串,主键建立索引查询效率低。常见优化方案为“转化为两个uint64整数存储”或者“折半存储”(折半后不能保证唯一性)
  • UUID还有一个缺点是,无法保证主键趋势递增

方法四:类snowflake算法

  • 缺点是在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步,有时候也会出现不是全局递增的情况。

容量设计

常见的容量评估包括数据量、并发量、带宽、CPU/MEM/DISK等,今天分享的内容,就以【并发量】为例

互联网架构设计如何进行容量评估:

  • 步骤一:评估总访问量
    ->询问业务、产品、运营
  • 步骤二:评估平均访问量QPS
    ->除以时间,一天算4w秒
  • 步骤三:评估高峰QPS
    ->根据业务曲线图来
  • 步骤四:评估系统、单机极限QPS
    ->压测很重要

线程数据设置多少合理

结论:
N核服务器,通过执行业务的单线程分析出本地计算时间为x,等待时间为y,则工作线程数(线程池线程数)设置为 N*(x+y)/x,能让CPU的利用率最大化。

经验:
一般来说,非CPU密集型的业务(加解密、压缩解压缩、搜索排序等业务是CPU密集型的业务),瓶颈都在后端数据库,本地CPU计算的时间很少,所以设置几十或者几百个工作线程也都是可能的。

单点系统架构

shadow-master是一种很常见的解决单点高可用问题的技术方案。

影子master”,顾名思义,服务正常时,它只是单点master的一个影子,在master出现故障时,shadow-master会自动变成master,继续提供服务。

  • master与shadow-master之间有一种存活探测机制
  • master与shadow-master有相同的虚IP(virtual-IP)
  • 当发现master异常时,shadow-master会自动顶上成为master,虚IP机制可以保证这个过程对调用方是透明的
  • 传统的一主多从,读写分离的db架构,只能保证读库的高可用,是无法保证写库的高可用的,要想保证写库的高可用,也可以使用上述的shadow-master机制。需要说明的是,由于数据库的特殊性,数据同步需要时延,如果数据还没有同步完成,流量就切到了shadow-master,可能引起小部分数据的不一致。

减少与单点的交互,是优化单点的系统的核心方向

  • 批量写:利用ID生成服务,每次从数据库拿出100个ID
  • 客户端缓存:以Goole File System为例
    (1)GFS的调用客户端client要访问shenjian.txt,先查询本地缓存,miss了
    (2)client访问master问说文件在哪里,master告诉client在chunk3上
    (3)client把shenjian.txt存放在chunk3上记录到本地的缓存,然后进行文件的读写操作
    (4)未来client要访问文件,从本地缓存中查找到对应的记录,就不用再请求master了,可以直接访问chunk-server。如果文件发生了转移,chunk3返回client说“文件不在我这儿了”,client再访问master,询问文件所在的服务器。
  • 尽量水平扩展。遗憾的是,并不是所有的业务场景都可以水平拆分,例如秒杀业务,商品的条数可能不多,数据库的数据量也不大,这就不能通过水平拆分来提升秒杀系统的整体写性能(总不能一个库100条记录吧?)。

一分钟了解负载均衡的一切(负载均衡)

负载均衡(Load Balance)通常是指,将请求/数据【均匀】分摊到多个操作单元上执行,负载均衡的关键在于【均匀】。
(1)【客户端层】到【反向代理层】的负载均衡,是通过“DNS轮询”实现的
(2)【反向代理层】到【站点层】的负载均衡,是通过“nginx”实现的
(3)【站点层】到【服务层:如dubbo】的负载均衡,是通过“服务连接池”实现的
(4)【数据层】的负载均衡,要考虑“数据的均衡”与“请求的均衡”两个点,常见的方式有“按照范围水平切分”与“hash水平切分”

lvs为何不能完全替代DNS轮询(DNS轮询)

  • DNS轮询tomcat
    如果DNS直接轮询Tomcat服务器,则保证不了高可用,因为它轮询后不保证解析出的IP对应的tomcat服务是可用的,所以基本上不用DNS作负载均衡
  • DNS+(nginx+keepalived)
    两台nginx都布署keepalived,并形成相同的虚拟IP,一台nginx挂了,另外一台另顶上,实现了高可用。
    此时的问题是,nginx是单点,会有性能瓶颈,而且资源利用率只有50%。
  • DNS+(lvs+keepalived)或(f5+keepalived)+nginx集群
    LVS(Linux virtual server)实施在操作系统层面,f5(特别贵)实施在硬件层面,它们的性能都比nginx高很多。
    这种实现方式能解决全球99.9999%的公司的访问量。
  • DNS+LVS集群+nginx集群
    这个和不用DNS作负载均衡有些矛盾?可能是只要LVS的每个服务都能保证是可用的,那么就可以DNS作负载均衡。
    同理,如果nginx也能保证每个服务是永远可用的,是不是也可以用DNS作nginx集群的负载均衡???
  • 建议:可以将服务部署在阿里云上,前端购买SLB服务

如何实施异构服务器的负载均衡及过载保护?(异构服务器)

负载均衡是均摊任务的, 如果有些服务对应的服务器性能比较差,势比会导致性能差的服务器扛不住。

  • 负载均衡时静态配置各服务器的权重
  • 动态权重设计
    1)用一个动态权重来标识每个service的处理能力,默认初始处理能力相同,即分配给每个service的概率相等;
    2)每当service成功处理一个请求,认为service处理能力足够,权重动态+1;
    3)每当service超时处理一个请求,认为service处理能力可能要跟不上了,权重动态-10(权重下降会更快);
    4)为了方便权重的处理,可以把权重的范围限定为[0, 100],把权重的初始值设为60分。
  • 过载保护
    互联网软件架构设计中的过载保护,是指当系统负载超过一个service的处理能力时,如果service不进行自我保护,可能导致对外呈现处理能力为0,且不能自动恢复的现象。而service的过载保护,是指即使系统负载超过一个service的处理能力,service能保证对外提供无损的稳定服务。
  • 借助动态权重实施一些策略对“疑似过载”的服务器进行降压,例如:
    1. 如果某一个service的连接上,连续3个请求都超时,即连续-10分三次,客户端就可以认为,服务器慢慢的要处理不过来了,得给这个service缓一小口气,于是设定策略:接下来的若干时间内,例如1秒(或者接下来的若干个请求),请求不再分配给这个service。
    2. 如果某一个service的动态权重,降为了0(像连续10个请求超时,中间休息了3次还超时),客户端就可以认为,服务器完全处理不过来了,得给这个service喘一大口气,于是设定策略:接下来的若干时间内,例如1分钟(为什么是1分钟,根据经验,此时service一般在发生fullGC,差不多1分钟能回过神来),请求不再分配给这个service;
    3. 可以有更复杂的保护策略…

需要注意的是:要防止客户端的过载保护引起service的雪崩,如果“整体负载”已经超过了“service集群”的处理能力,怎么转移请求也是处理不过来的,还得通过抛弃请求来实施自我保护

究竟啥才是互联网架构“高并发”(高并发)

从两个方面提升系统的高并发

1.、垂直扩展

  1. 增强单机硬件性能,例如:增加CPU核数如32核,升级更好的网卡如万兆,升级更好的硬盘如SSD,扩充硬盘容量如2T,扩充系统内存如128G;
  2. 提升单机架构性能,例如:使用Cache来减少IO次数,使用异步来增加单服务吞吐量,使用无锁数据结构来减少响应时间;

2、水平扩展:增加服务器数量

  1. 反向代理层:增加nginx服务器,通过DNS轮询访问不同的服务
  2. 服务层:增加service服务,通过nginx的nginx.conf配置负载均衡
  3. 数据库:增加数据库。将数据按照某个存储字段进行hash后,不同的值存在不同的数据库。例如,用uuid求模,数据库数量保持2的n次幂就能做到平滑

究竟啥才是互联网架构“高可用”(高可用)

保证高可用的原则是“集群化”,或者叫“冗余”,最终通过“自动故障转移”来实现系统的高可用
具体实现如下:

(1)反向代理层:通过反向代理层的冗余实现的,常见实践是keepalived + virtual IP自动故障转移
(2)站点应用层:通过站点层的冗余实现的,常见实践是nginx与web-server之间的存活性探测与自动故障转移
(3)服务层:通过服务层的冗余实现的,常见实践是通过service-connection-pool来保证自动故障转移
(4)数据-缓存层:通过缓存数据的冗余实现的,常见实践是缓存客户端双读双写,或者利用缓存集群的主从数据同步与sentinel保活与自动故障转移;更多的业务场景,对缓存没有高可用要求,可以使用缓存服务化来对调用方屏蔽底层复杂性
(5)数据-数据库层读库是通过读库的冗余实现的,常见实践是通过db-connection-pool来保证自动故障转移。写库是通过写库的冗余实现的,常见实践是keepalived + virtual IP自动故障转移

100亿数据1万属性数据架构设计(数据架构设计)[理解不透]

参考:https://www.w3cschool.cn/architectroad/architectroad-data-architecture-design.html
以目前的理解,【统一类目属性服务】这一小节讲解中缺少一个类目表,应该可以是省市地域的类似层级关系。

优化反向依赖(反向依赖解耦)

1.哪些情况算是反向依赖?

类似需求方是A,改动方确是BCDE的情况就算是反向依赖

2.常见的反向依赖

(1)公共库(公共类)导致耦合
        优化一:公共库导了耦合,说明里面的方法并不能作为公共方法,此时需要拆成多个公共库。
        优化二:如果公共库是业务共性代码,可以将公共库服务化,然后供多个业务层调用。

(2)服务化不彻底导致耦合
        特征:服务中包含大量“根据不同业务,执行不同的个性代码
        优化方案:个性代码放到业务层实现,让服务化更彻底更纯粹

(3)notify的不合理实现导致的耦合
        特征:调用方不关注执行结果,以调用的方式去实现通知,新增订阅者,修改代码的是发布者
        优化方案:通过MQ解耦

(4)配置中的ip导致上下游耦合
        特征:多个上游需要修改配置重启
        优化方案:使用内网域名替代内网ip,通过“修改DNS指向,统一切断旧连接”的方式来上游无感切换

(5)下游扩容导致上下游耦合
        特性:多个上游需要修改配置重启

典型数据库架构设计

  1. 业务初期用单库
  2. 如果读压力大,读需要高可用。可通过一主多从(本质上是将数据进行复制)实现读写分离。
  3. 如果数据量大,写需要线性扩容时,可水平拆分。水平拆分强烈建议使用分库而不是分表,分表用的还是同一个数据库文件,还是会有磁盘IO的竞争。水平拆分常用算法朋“范围法”和”哈希法
    如果数据量大,读写并发量都很高,则需要水平拆分后,再实现一主多从。

  4. 将一个表的多个字段分开在不同的表里存储。属性短,访问频度高的属性,拆分到一起。虽然可减少磁盘IO,但是太复杂了,个人不建议使用。

典型架构实践

TCP接入层

配置中心架构设计

配置架构如何演进?
一、配置私藏
二、全局配置文件:文件监控组件FileMonitor、动态连接池组件DynamicConnectionPool
三、配置中心(zookeeper服务):如果配置中心挂了就完了。

主要解决的问题是:反向依赖。

跨公网调用架构设计

分布式session架构设计

  • session同步法
    思路:多个web-server之间相互同步session,这样每个web-server之间都包含全部的session
    优点:web-server支持的功能,应用程序不需要修改代码
    不足
    • session的同步需要数据传输,占内网带宽,有时延
    • 所有web-server都包含所有session数据,数据量受内存限制,无法水平扩展
    • 有更多web-server时要歇菜

  • 客户端存储法
    思路:服务端存储所有用户的session,内存占用较大,可以将session存储到浏览器cookie中,每个端只要存储一个用户的数据了
    优点:服务端不需要存储
    缺点
    • 每次http请求都携带session,占外网带宽
    • 数据存储在端上,并在网络传输,存在泄漏、篡改、窃取等安全隐患
    • session存储的数据大小受cookie限制
    “端存储”的方案虽然不常用,但确实是一种思路。

  • 反向代理hash一致性
    方案一:四层代理hash(比方案2好
    反向代理层使用用户ip来做hash,以保证同一个ip的请求落在同一个web-server上
    方案二:七层代理hash
    反向代理使用http协议中的某些业务属性来做hash,例如sid,city_id,user_id等,能够更加灵活的实施hash策略,以保证同一个浏览器用户的请求落在同一个web-server上
    优点
    • 只需要改nginx配置,不需要修改应用代码
    • 负载均衡,只要hash属性是均匀的,多台web-server的负载是均衡的
    • 可以支持web-server水平扩展(session同步法是不行的,受内存限制)
    不足
    • 如果web-server重启,一部分session会丢失,产生业务影响,例如部分用户重新登录
    • 如果web-server水平扩展,rehash后session重新分布,也会有一部分用户路由不到正确的session
    session一般是有有效期的,所有不足中的两点,可以认为等同于部分session失效,一般问题不大。

  • 后端统一存储
    思路:将session存储在web-server后端的存储层,数据库或者缓存。推荐存缓存,毕竟session是可以丢失的数据。
    优点
    • 没有安全隐患
    • 可以水平扩展,数据库/缓存水平切分即可
    • web-server重启或者扩容都不会有session丢失
    不足:增加了一次网络调用,并且需要修改应用代码

数据库与缓存

数据库架构需要设计什么 ?

数据库架构设计,需要考虑高可用、一致性、扩展性、读性能等等

  1. 高可用

    • 写高可用
      双主互备的方式实现写的高可用。
      影子服务。虽是双主,但只有一个主提供服务(读+写),另一个主是“shadow-master”,只用来保证高可用,平时不提供服务。master挂了,shadow-master顶上(vip漂移,对业务层透明,不需要人工介入)
    • 读高可用
      添加多个从库,实现冗余读库
  2. 一致性

    • 中间件:如果某一个key有写操作,在不一致时间窗口内,中间件会将这个key的读操作也路由到主库上。一般大公司才有能力开发这个中间件。
    • 强制读主库
    • 数据库与缓存间的不一致:在一些异常时序情况下,有可能【从库读到旧数据(同步还没有完成),旧数据入cache后】,数据会长期不一致。
      利用缓存双淘汰:先淘汰旧cache,再写入库,主从同步完之后,再淘汰一次旧缓存。建议为所有cache中的item设置一个超时时间
  3. 扩展性

    • 扩展容量:每次扩容,都乘以2倍的关系增加。
  4. 读性能

    • 增加从库
    • 增加缓存
    • 服务+数据库+缓存一套(58的方案,没有细讲)

冗余表的数据一致性

  1. 为什么需要冗余表?
    数据库因为数据量太大时会进行水平拆分,拆分时会用利用某一个字段(partion key)的相关规则进行拆分,如果我们不是搜索partion key,那么就会搜索多个库。

  2. 需求示例
    例如订单表,业务上对买家和卖家都有订单查询需求:
    Order(oid, info_detail)
    T(buyer_id, seller_id, oid)
    如果按buyer_id来分库,seller_id的查询就需要扫描多库。
    如果按seller_id来分库,buyer_id的查询就需要扫描多库。

  3. 实现思路
    这类需求,为了做到高吞吐量低延时的查询,往往使用“数据冗余”的方式来实现,就是文章标题里说的“冗余表”:
    T1(buyer_id, seller_id, oid)
    T2(seller_id, buyer_id, oid)
    同一个数据,冗余两份,一份以buyer_id来分库,满足买家的查询需求;一份以seller_id来分库,满足卖家的查询需求。
    需要特别说明的是,T1和T2要分别存在各自的数据库里。如果按buyer_id分库需要10个数据库,按seller_id分库需要6个数据库,则最终需要16个数据库,这比只按某一个字段分库要多用许多的数据库

  4. 注意问题
    尽量保证一个库里插入成功了,另外一个库也要插入成功,避免数据不一致。

  5. 保证两次插入都成功的解决方案
    参考:https://www.w3cschool.cn/architectroad/architectroad-redundant-table.html

缓存架构及其与数据库的一致性

  1. 缓存架构
    采用服务化:加入一个服务层,向上游提供帅气的数据访问接口,向上游屏蔽底层数据存储的细节,这样业务线不需要关注数据是来自于cache还是DB。
    需要注意的是,如果数据要更改,最好是先淘汰缓存,再更新数据库。这样可以避免,更数据库成功,删除缓存失败而导致缓存的数据一直是旧数据。

  2. 导致缓存不一致的原因
    没有从库的产生原因:在读写都很频繁的系统中,如果A想更新数据,则A会先清掉缓存,再更新数据库;问题来了,在A清掉缓存成功还未进行数据库更新时,B就来读缓存了,此时缓存被A清掉了,B就从数据库去读数据;而此时呢,A还没有将最新的数据更新到数据库中,所以就会导致B又将旧数据读到了缓存当中,最终导致了缓存与数据库的不一致。

    有从库的产生原因:主从同步时,一般是读写分离(主写从读),当A先淘汰缓存后,再在主库更新,B此时去缓存拿不到数据,就去从库将旧数据又放到缓存里了,而后主库的更新才同步到从库,此时就造成了缓存与数据库的不一致。

  3. 只有主库时的缓存不一致解决方案

    第一步:修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上。取模时要把不可用的服务连接排除掉。

    第二步:修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的

    其实,个人认为也可以利用下面所说的 缓存双淘汰 解决。只是这里在第二次淘汰缓存时不需要等待一段时间。

  4. 主从同步,读写分离时的缓存不一致解决方案
    此时的不一致主要是因为主从数据同步的延时造成的,所以我们可以在更新完主库后,间隔一段时间(1s左右,具体根据实际情况定)再次淘汰此数据的缓存——缓存双淘汰。已有的实现方式:
    (1)timer异步淘汰:本质就是起个线程专门异步延时二次淘汰缓存
    (2)总线异步淘汰
    (3)读binlog异步淘汰:分析主库的binlog的不一致的地方,找到不一样的地方,从库同步之后,再次淘汰对应缓存。只是这里的第二次缓存淘汰不需要等待一段时间。

DB主从一致性(延时性)架构优化4种方法

其实在第一次看这篇文章的时候,总觉得 方法一 哪里不对,后来我请教了我的老大才明白,这篇文章更准确的来说讲的应该是“延时性”,而不是一致性。下面我举一个实例:

用户在下完订单返回成功后的一瞬间(写完库但数据还未同步到从库),商家此时就去从库查询订单,商家此时是查询不到订单的。逻辑上,用户下完单成功了,商家就应该能查询到订单的。

  1. 解决办法1:半同步复制
    写或改操作在主从完成同步之后才返回“操作成功”,而不是写完主库就返回操作成功。这样就满足了逻辑上的“添加成功之后就一定能查到最新的数据”。这样做的缺点是,写操作的时间变得更长了。

  2. 解决办法2:强制读主库
    这种读写都完全落到主库上的办法不太可行

  3. 解决办法3:使用数据库中间件
    所有的读写请求都走数据库中间件,通常情况下,写请求路由到主库,读请求路由到从库。如果数据有更新或插入,将对应的key记录保存,在遇到对应key的读请求时,在主从未完全同步这段时间内,读操作都路由到主库。

  4. 解决办4:缓存记录写key法【简单且有效,推荐】
    如果发生了写或修改操作,将对应key记录在cache里作为标记,并设置“主从同步经验时间”为cache的超时时间(一般比那个要大点儿,以保证主从同步完成)。如果读操作在缓存里命中对应key,则说明主从同步还未完成,此时就将读操作路由到对应主库;如果没有命中,则直接路由到从库。

如果,架构设计既要保证缓存一致,又要保证主从数据的一致性(实时性),则需将这节和上节的处理办法融合。

多库多事务

数据量大时,一个完整的业务流程就会涉及到多个数据库,这时大事务里就会有很多小事务。如何保证完整的事务呢?

  • 补偿事务:如果大事务有很多子事务,第一个子事务提交成功了才执行第二个子事务,依次类推,如果有一个事务失败了,则将之前提交的子事务都执行一个逆向操作(是插入操作就执行删除操作)。
    缺点:写代码复杂。在重启和执行逆向操作的时候还是有可能导致数据不一致。

  • 后置提交:由于事务的提交只占很少很少的时间,所以我们可以将所有子事全都执行完成后,再统一挨个执行事务提交。此时可能出现异常导致回滚的时间只有在最后的统一提交的这段很小的时间内,所以此方法大大降低了数据不一致的概率。
    缺点:所有库的连接,要等到所有事务执行完才释放。系统整体的吞吐量降低了。在重启和执行提交的时候,有可能导致数据不一致。

其实最好参考分布式事务:https://m.aliyun.com/yunqi/articles/542020

mysql并行复制[理解不透]

什么是relaylog? relay-log日志记录的是在复制过程中,从服务器的I/O线程将主服务器的二进制日志读取过来记录到从服务器本地的文件,然后SQL线程会读取relay-log日志的内容并应用到从服务器。

并行复制的目的是降低主从同步的延时问题。实现方式:

  • 多线程并行重放relaylog来缩短同步时间:相同库上的写操作,用相同的work-thread来重放relaylog;不同库上的写操作,可以用多个work-thread并发来重放relaylog。实现方式如下:
    hash(db-name) % thread-num,即库名hash之后再模上线程数。

  • 基于GTID的并行复制【mysql版本至少5.6】

具体参见:https://www.w3cschool.cn/architectroad/architectroad-mysql-parallel-copy.html

快速恢复删掉的数据库

如果人为不小心执行了“删全库”操作,命令会同步给其他从(主)库,导致所有库上的数据全部丢失,这下怎么办呢?
(1)全量备份+增量备份,并定期进行恢复演练。该方案缺点是恢复时间较久,对系统可用性影响大
(2)1小时延时从:增加一台从库,每隔1小时才复制一次主库的操作,相当于该从库的操作日志比主库延后了一小时。该方案的问题是,如果删除操作就发生在从库连接上主库的时候,那就玩完了。
(3)双份1小时延时从:两台从库相差半小时(或更多时间)连接主库。这样既然避免方案2的问题,又能极大加速数据库恢复时间,但是资源利用率变低了。
(4)个人建议1小时延时从足够,后台只读服务可以连1小时延时从(前提是后台服务对实时性要求不高),从而提高资源利用率。

为数据库表增加新字段的技巧

  1. 那些不可行的方案

    • alter table add column (大数据高并发情况下,锁表时间长,一定不可行)
    • 通过增加表的方式扩展,通过外键join来查询(大数据高并发情况下,join性能较差,一定不可行)
    • 通过增加表的方式扩展,通过视图来对外(大数据量是禁止使用视图的)
    • 打产品经理(比较靠谱)
  2. 成熟的方案:新表+触发器+迁移数据+rename(pt-online-schema-change)

    • 以user(uid, name, passwd) 扩展到 user(uid, name, passwd, age, sex)为例 说明该方案的实施步骤
      (1)先创建一个扩充字段后的新表user_new(uid, name, passwd, age, sex)
      (2)在原表user上创建三个触发器,对原表user进行的所有insert/delete/update操作,都会对新表user_new进行相同的操作
      (3)分批将原表user中的数据insert到新表user_new,直至数据迁移完成
      (4)删掉触发器,把原表移走(默认是drop掉)。(第4步和第5步需要写脚本,这样能将没有user表的时间尽量缩短。如果是银行系统,必须得停服务,防止在删除旧表得到新表这段时间内造成的数据不一致问题 )
      (5)把新表user_new重命名(rename)成原表user

    • 优点:整个过程不需要锁表,可以持续对外提供服务

    • 操作注意事项
      (1)变更过程中,最重要的是冲突的处理,一条原则,以触发器的新数据为准,这就要求被迁移的表必须有主键(这个要求基本都满足)
      (2)变更过程中,写操作需要建立触发器,所以如果原表已经有很多触发器,方案就不行(互联网大数据高并发的在线业务,一般都禁止使用触发器)
      (3)触发器的建立,会影响原表的性能,所以这个操作建议在流量低峰期进行

  3. 其他方案
    (1)版本号version + 扩展字段ext(部分字段无法建立索引)
    (2)所有字段与值都用key+value方式存储,扩充属性时,只需要添加一行就行(同一条数据的uid相同)。我们项目中动态字段就是用这个方式设计的
    (3) 提前预留一些reserved字段(但如果预留过多,会造成空间浪费,预留过少,不一定达得到扩展效果)
    (4)通过增加表的方式扩展列,上游通过service来屏蔽底层的细节(需要join,而数据量大时join效率较低)

数据库的垂直和水平拆分

垂直拆分也有 将不同的业务数据放在不同的数据库上的意思。具体回去看看书?????????????????????????

  1. 垂直拆分
    将一个属性较多,一行数据较大的表,将不同的属性拆分到不同的表中,以降低单库(表)大小。

    • 垂直拆分特点
      (1)每个库(表)的结构都不一样
      (2)一般来说,每个库(表)的属性至少有一列交集,一般是主键
      (3)所有库(表)的并集是全量数据

    • 拆分方法
      (1)将长度较短,访问频率较高的属性尽量放在一个表里,这个表暂且称为主表
      (2)将字段较长,访问频率较低的属性尽量放在一个表里,这个表暂且称为扩展表
      如果1和2都满足,还可以考虑第三点:
      (3)经常一起访问的属性,也可以放在一个表里
      如果实在属性过多,主表和扩展表都可以有多个
      当应用方需要同时访问主表和扩展表中的属性时,服务层不要使用join来连表访问,而应该分两次进行查询

    • 为何要将字段短,访问频率高的属性放到一个表内?为何这么垂直拆分可以提升性能?
      (1)数据库有自己的内存buffer,会将磁盘上的数据load到内存buffer里(暂且理解为进程内缓存吧)
      (2)内存buffer缓存数据是以row为单位的
      (3)在内存有限的情况下,在数据库内存buffer里缓存短row,就能缓存更多的数据
      (4)在数据库内存buffer里缓存访问频率高的row,就能提升缓存命中率,减少磁盘的访问

  2. 水平拆分推荐
    以某个字段为依据(例如uid),按照一定规则(例如取模),将一个库(表)上的数据拆分到多个库(表)上,以降低单库(表)大小

    • 水平拆分特点
      (1)每个库(表)的结构都一样
      (2)每个库(表)的数据都不一样,没有交集
      (3)所有库(表)的并集是全量数据

    • 拆分方法
      (1) 范围法:类似按主键的取值范围分摊在不同的数据库表。缺点是数据访问可能会集中在某些库表。
      (2) 哈希法:类似将按主键取模后,存放在不同的数据库表。扩容时要以2的指数倍扩,以防止产生数据迁移问题。

  3. 水平拆分的问题对非uid字段的查询不好实现

    • 用户端一般只有一两个字段需要查询,这时可以通过建立 非uid字段——>uid 的映射关系,实现方式有:
      • 索引表法:数据库中记录login_name->uid的映射关系
      • 缓存映射法:缓存中记录login_name->uid的映射关系(推荐
      • login_name生成uid
      • login_name基因融入uid

    • 像运营台这种会有千奇百怪的查询要求时,可通过以下几种方式实现:
      • 前、后台分离出来,避免后台低效查询引发前台查询抖动,(通过MQ或者线下异步同步数据,不能实时访问库)
      • 可以采用“外置索引”(例如ES搜索系统)或者“大数据处理”(例如Hive)来满足后台变态的查询需求(推荐

数据库秒级平滑扩容

  1. 数据库每一次性扩充后,数据库数量最终都为2的指数倍
    %2=0的库,会变为%4=0与%4=2;
    %2=1的库,会变为%4=1与%4=3;
    下面的链接的讲解中有个重要的遗漏点,如里面所讲的扩充为4台时,那么最初对2求模的数据会全部同步到user(ip00)这台数据库,然后又会删除掉user(ip0)中对2求模为2的数据,接着又会删除掉user(ip00)数据库中对2求模为0的数据。另外两台数据库作类似的操作。
    具体参考:https://www.w3cschool.cn/architectroad/architectroad-database-smooth-expansion.html

数据平滑迁移

可以参考前面讲表的字段变更的数据迁移方案 (新表+触发器+迁移数据+rename)

  1. 场景
    (1)底层表结构变更
    (2)分库个数变化
    (3)数据库类型变化
  2. 追日志法,五个步骤:
    (1)服务进行升级,记录“对旧库上的数据修改”的日志
    (2)研发一个数据迁移小工具,进行数据迁移
    (3)研发一个读取日志小工具,追平数据差异
    (4)研发一个数据比对小工具,校验数据一致性
    (5)流量切到新库,完成平滑迁移

  3. 双写法,四个步骤:
    (1)服务进行升级,记录“对旧库上的数据修改”进行新库的双写
    (2)研发一个数据迁移小工具,进行数据迁移
    (3)研发一个数据比对小工具,校验数据一致性
    (4)流量切到新库,完成平滑迁移

我很想知道,服务进行升级(要添加代码),不需要重启吗?

数据库军规

还是看原文吧:
https://www.w3cschool.cn/architectroad/architectroad-58-home-database-rules.html
https://www.w3cschool.cn/architectroad/architectroad-58-home-database-rules-2.html

跨库分页

通过二次查询法解决,有一点儿疑问是,如果数据库多达几十台或更多,也这样做吗?
具体参考:https://www.w3cschool.cn/architectroad/architectroad-cross-database-paging.html

数据库中间件(mysql-proxy)

参考原文:https://www.w3cschool.cn/architectroad/architectroad-mysql-proxy.html

服务化与微服务

意义不大,想了解可参考原文

消息系统

http实时接收消息【长连接】

利用http长连接的长轮询实现实时接收消息。该长连接不是一直不断开,而是快速的停下然后又立即开始连接。
具体参考:https://www.cnblogs.com/hoojo/p/longPolling_comet_jquery_iframe_ajax.html

保证在线消息的可靠传递

具体参考 :https://www.w3cschool.cn/architectroad/architectroad-the-reliable-delivery-of-instant-messaging.html

  • im系统是通过超时、重传、确认、去重的机制来保证消息的可靠投递,不丢不重
  • 一个“你好”的发送,包含上半场msg:R/A/N与下半场ack:R/A/N的6个报文
  • im系统难以做到系统层面的不丢不重,只能做到业务层面的不丢不重

R(request:请求报文):客户端主动发送给服务器的报文
Aacknowledge:应答报文):服务器被动应答客户端的报文,一个A对应一个R
N(notify:通知报文):服务器主动发送给客户端的报文

保证离线消息的可靠传递

微信是不保存记录的,QQ貌似要保存所有消息。

  1. 接收方不在时,消息先存在数据库中。

  2. 拉取离线消息时,应当是在确定拉取成功之后再删除DB中的数据,而不是先删除再返回。即在ack之后再删除。

  3. 用户好友太多,如何减少与后台交互次数?

    • 先拉取各个好友的离线消息数量,真正用户B进去看离线消息时,才往服务器发送拉取请求
    • 一次性拉取所有好友发送给用户B的离线消息,到客户端本地再根据sender_uid进行计算。这样的话,离校消息表的访问模式就变为->只需要按照receiver_uid来查询了。登录时与服务器的交互次数降低为了1次。当然,这一次拉取最好只是拉一个分页量上的数据,具体点击某个好友了才继续拉取。
  4. 如果在ack之前出了问题,重新登录时会出现重复拉取离线消息怎么破?
    在业务层面,可以根据msg_id去重。SMC理论:系统层面无法做到消息不丢不重,业务层面可以做到,对用户无感知。

  5. 假设有N页离线消息,现在每个离线消息需要一个ACK,那么岂不是客户端与服务器的交互次数又加倍了?有没有优化空间?
    因为拉取是分页拉取的,所以建议只在拉取最后一页时才ack,其它都是在拉取当前页的同时删除DB中上一页的离线数据。

在线及离线群消息

  1. 用户的在线及离线状态存在缓存当中的。

  2. 每次用户拉取消息时,都是拉取最后一次ack时的时间或对应群消息ID(msg_id)之后的数据。

  3. 减少ack请求量
    分页拉取,只在最后一页数据需要ack。可以每收到N条群消息ACK一次,也可以每隔时间间隔T进行一次群消息ACK。

  4. 收到重复消息也通过msg_id去重。

  5. 群消息涉及到的表

    • 群消息表:用来存储一个群中所有的消息内容
      t_group_msgs(group_id, sender_id, time,msg_id, msg_detail)

    • 群成员表:用来描述一个群里有多少成员,以及每个成员最后一条ack的群消息的msg_id(或者time)
      t_group_users( group_id, user_id, last_ack_msg_id ( last_ack_msg_time ) )

QQ状态一致性

状态的实时性与一致性是一个较难解决的技术问题,不同的业务接受度,不同的数据量并发量在线量,实现方式不同,个人建议的方式是:

1)好友状态,如果对实时性要求较,可以采用推送的方式同步;如果实时性要求不高,可以采用轮询拉取的方式同步

2)群友的状态,由于消息风暴扩散系数过大,可以采用按需拉取,延时拉取的方式同步

3)系统消息/开屏广告等对实时性要求不高的业务,可以采用拉取的方式获取消息

4)“消息风暴扩散系数”是指一个消息发出时,变成N个消息的扩散系数,这个系数与业务及数据相关,一定程度上它的大小决定了技术采用推送还是拉取

剩下的在做聊天软件时细研究

消息总线架构

搜索架构

搜索架构引擎、方案与细节

站内和全网搜索引擎架构参考:https://www.w3cschool.cn/architectroad/architectroad-search-architecture.html

  1. 正排索引:由uid或url查询分词的过程,就是正排索引查询。可以理解为Map<url, list<item>>
  2. 倒排索引:由分词查询url或uid的过程,就是倒排索引。可以理解为Map<item, list<url>>
  3. 两个有序集合求交集算法:

    • for * for。时间复杂度:O(n*n)
    • 拉链法。复杂度:O(n)
    • 分桶并行优化。数据量大时,url_id分桶水平切分+并行运算是一种常见的优化方法,如果能将list1<url_id>list2<url_id>分成若干个桶区间,每个区间利用多线程并行求交集,各个线程结果集的并集,作为最终的结果集,能够大大的减少执行时间。
    • bitmap再次优化。bitmap优化之后,能极大提高求交集的效率,但时间复杂度仍旧是O(n),且bitmap需要大量连续空间,占用内存较大
    • 跳表skiplist。时间复杂度近似O(log(n)),这是搜索引擎中常见的算法。

快速实现搜索需示

(1)原始阶段- LIKE
(2)初级阶段- 全文索引(只支持MYISAM引擎)
(3)中级阶段- 开源外置索引(ElasticSearch、solr)
(4)高级阶段- 自研搜索引擎

实时检索

超大数据量,超高并发量,实时搜索引擎的两个架构要点:

  • 索引分级(小时库,天库、星期库、月库、全量库),可降低索引碎片
  • dump&merge

Tips:

  • dumper:将在线的数据导出
  • merger:将离线的数据合并到高一级别的索引中去
  • 当有修改请求发生时,只会操作最低级别的索引,例如小时库。
  • 当有查询请求发生时,会同时查询各个级别的索引,将结果合并,得到最新的数据:

架构实践

id串行化是怎么实现的

保证id串行化,即一个群gid的消息落能够到同一个服务器上处理,可以通过如下方式实现:

返回与业务id取模相关联的Service连接

需要注意的是,连接池不关心传入的long id是什么业务含义:
(1)传入群gid,同gid的请求落在同一个service上
(2)传入用户uid,同uid的请求落在同一个service上
(3)传入任何业务xid,同业务xid的请求落在同一个service上

从IDC到云端架构迁移之路

  1. 互联网单机房架构的特点,全连:站点层全连业务服务层,业务服务层全连的基础服务层,基础服务层全连数据库和缓存;
  2. 多机房架构的特点,同连:接入层同连服务层,服务层同连缓存和数据库,架构设计上最大程度的减少跨机房的调用;
  3. 自顶向下的机房迁移

    • 站点接入层、业务服务层和基础服务层的迁移:搭建服务,逐步的迁移流量;
    • 缓存迁移:搭建缓存,运维修改缓存的内网DNS指向,断开旧连接,重连新缓存,完成迁移;
    • 数据库的迁移:搭建数据库,数据进行同步,只读,保证数据同步完成之后,修改配置,重启,完成迁移。
      整个过程分批迁移,一个业务线一个业务线的迁移,一块缓存一块缓存的迁移,一个数据库一个数据库的迁移,任何步骤出现问题是可以回滚的,整个过程不停服务。
  4. 自底向上的机房迁移

一致性问题

库存扣减处理

  1. 为了防止系统的重试机制导致重复扣减,需要将在数据库层面的直接扣减变成数据库层面的修改
    业务场景:用户下单前库存num=5,用户购买量buyNums=3
    错误SQL:update stock set num=$buyNums-3 where sid=$sid
    实际上应该要先在服务层先计算出库存剩余量 num_new = 5-3=2
    正确SQL:update stock set num=$num_new where sid=$sid

  2. 为了防止高并发时多个修改操作都成功,可以加上原库存来做比较即CAS(Compare And Set)
    业务场景:高并发时,两个用户下单前查询库存num=5,用户A购买量buyNums=1,用户A购买量buyNums=2
    错误SQL:用户A执行update stock set num=4 where sid=$sid,然后用户B执行update stock set num=3 where sid=$sid,最终库存变成3,而实际应该是2。
    正确SQL:用户A执行update stock set num=4 where sid=$sid and num=5,然后用户B执行update stock set num=3 where sid=$sid and num=5。可以看到,用户A执行成功后,用户B就会执行失败,因为旧库存已经不是5了。

综上所述,平时写类似扣减的业务,需要用这样的SQL:update stock set num=新库存 where sid=$sid and num=旧库存

当然,也可以根据不同的业务使用如下方法:

  • 使用时间戳、版本号来保证一致性
  • 使用队列,在数据库层面串行执行,降低锁冲

CAS应用在分布式ID

  • 业务场景:分布式ID生成服务是冗余的,两个ID生成服务从主键表拿到的max_id是一样的,A服务将max_id更新成200后,B服务也能将max_id更新成200。
  • 错误SQL:update t set max_id=300 where id = xxx
  • 解决办法:update t set max_id=300 where id = xxx and max_id=200;
  • 慧种地系统的自己维护主键的代码就是这么实现的

CAS下的ABA问题及优化

  1. 极端情况下的ABA问题
    考虑如下操作:
    •线程1(上):获取出数据的初始值是A,后续计划实施CAS乐观锁,期望数据仍是A的时候,修改才能成功
    •线程2:将数据修改成B
    •线程3:将数据修改回A
    •线程1(下):CAS乐观锁,检测发现初始值还是A,进行数据修改
    上述并发环境下, 线程1在修改数据前,中间发生了A变B,B又变A的过程,此A已经非彼A,数据依旧能修改成功。这就可能导致错误,这就是CAS引发的所谓的ABA问题。

  2. 优化办法(加一个版本号)

    • 库存表由 stock(sid, num) 升级为 stock(sid, num, version)
    • 查询时将版本号查出来:select num, version from stock where sid=#{sid}
    • 更新时要比对版本号,且更新版本号为当前版本事情:
      update stock set num=#{num_new}, version=#{version_new} where sid=#{sid} and version=#{version_old}

猜你喜欢

转载自blog.csdn.net/zcl_love_wx/article/details/80245714