本篇主要回答以下问题:

  • 读写分离是什么?

  • MySQL 提供了哪些复制模式?分别对应应用场景?

  • 什么场景下需要读写分离?

  • 怎么进行读写分离?

  • 怎么避免主从复制延迟?

  • 什么场景下需要分表分库?

  • 分表解决什么问题?分库解决什么问题?

  • 分表分库产生什么问题?

  • 分库分表有什么样的拆分方式?分片键sharedkey 怎么选择?

  • 分表分库后如何保证记录ID全局唯一?

  • 基于时间生成ID方案发生时间回拨怎么办?

  • MyCAT 数据库代理中间件

  • 什么时候用到分布式锁?有哪些方案?

  • 怎么用redis 实现分布式锁?

  • 基于Redis 实现分布式锁有那些缺陷?

  • zookeeper 下的分布式锁

  • 基于 redis 实现的分布式锁 和 zookeeper 实现的分布式锁 对比

  • 基于MySQL 是实现的分布式锁

读写分离是什么?

将数据库读请求和写请求分离的以一种技术方案. MySQL 主从同步默认实时 异步模式.

主从复制原理

  • 主节点收到一个 增删改事务,写入redolog, 写binlog

  • 进入两阶段提交,启动dumplog 线程给binlog 加锁,读取主机binlog 日志,读取完成后释放锁,发送binlog 消息给订阅的从机

  • 丛机启动好的dumplog 线程,接收发送过来的binlog,将消息写道reply log 中

  • 从机SQL 线程读取reply log 重放数据操作。完成数据同步

过程中,binlog、replylog 都是顺序读写

关于主从复制binlog 格式

binlog 有三种日志格式

  • statement: SQL 语句

  • row: 变更后的数据行

  • mix: 混合,row和statement

binlog 格式注意事项

statement 格式 在搭配 主键自增模式 innodb_autoinc_lock_mode = 0 (主键自增列自增后立即释放数据行上主键列的锁) 时 会出现主从数据不一致的情况,这个一般出现在,主从/主-主备 双写的情况,因为这个配置模式下的主从复制无法保证主从复制事务的原子性。具体示例见下:

偷个小林哥的图 MySQL 有哪些锁? | 小林coding

  • 主库 自增从0 开始,插入四条数据 (0,1,1) (1,2,2) (2,3,3) --插入中-> (3,4,4) 【主键自增模式 innodb_autoinc_lock_mode = 0 (主键自增列自增后立即释放数据行上主键列的锁)

  • 从库创建表,自增从0 开始,插入两条数据 (1,1,1) (2,2,2)

  • 此时主从同步,从库同步主库binlog 后得知主库自增ID 最后已分配ID = 3,所以下一个待分配ID = 4

  • 从库插入两记录 (id1,3,3) (id2,4,4) 此时从库这两条插入id 分配情况 id1 = 4、id2 = 5 对应记录: (4,3,3) (5,4,4) -- 出现主从数据不一致

OS 这个出现情况应该是出现在异步和半异步的情况....

MySQL 提供了那些复制模式?

  • 同步模式、异步模式、半异步模式(半同步模式)、半异步增强模式

同步模式

  • 事务双写阶段完成后,启动一个dumplog 线程,Binlog 中的内容发送给订阅的从机(并开始等待从机恢复ack -- 确认从机复制完成)

  • (从机在连接完成后就会启动一个dumplog) 从机接收 来自主机的增量消息或全量消息,并将接收数据写入reply log 中

  • 写入完成后回复主机 ack

  • 主机提交事务,返回服务成告知事务已提交(执行成功)

优点:

  • 最高的一致性,主从同步间所有事务都会等待主从同步完成后再能继续

缺点:

  • 性能最差,不仅要等待网络传输,还要等待所有从机写完才能提交事务返回服务层

异步模式

  • 主机dumplog 发日志,不等从机dumplog 回 ack,主机写道binlog 算事务提交。

缺点:

  • 一致性保障较差, 如果主库未来得及发送binlog 消息给从库就挂掉就回丢失最新的消息,新上来的热主备,是没有挂掉前待同步的消息的,这就出现数据不一致了

优点

  • 适合做备份,对于数据实时性不敏感的场景下,可用作读写分离

半异步模式

  • 主机dumplog 发送 binlog 消息给从机,等待并统计从机返回消息

  • 丛机接收到消息,写reply log, 返回ack

  • 主机接收ack, 统计ack 计数,计数过半 (计数,从机数量),将事务设置为已提交,返回服务 -- 这个计数返回阈值,可以设置的

缺点:

  • 部分从机可能和主机数据数据尚未同步

优点:

  • 主从数据一致性有较强保障,且性能较高。

增强半异步(半同步)

开启此项配置: rpl_semi_sync_master_wait_point = AFTER_SYNC

  • 主机dumplog 发送 binlog 消息给从机,等待并统计从机返回消息

  • 丛机接收到消息,写reply log并重放主机数据操作 之后才返回ack

  • 主机接收ack, 统计ack 计数,计数过半 (计数,从机数量),将事务设置为已提交,返回服务 -- 这个计数返回阈值,可以设置的

优缺点同上半异步,但比半同步更慢,因为要等至少一台从机数据回放完成才返回服务层.

分别对应那些场景可以用?

  • 全同步银行,证券,对数据一致性和实时性要求高的

  • 半同步(半异步):电商,商品库存数据。对展示商品库存实时性要求不是那么的,此时可用读写分离

  • 异步,评论数据,小说,电影数据,此时可用读写分离

什么场景下需要读写分离?

读写分离的目的是为了缓解单点I/O 压力,采取的读写分流手段。一般将读请求单独分流到一个读表/库中,响应读查询。将写请求单独分流到一个写表/库中做写操作,这样可以减少数据竞争,从分利用MySQL 缓存池。读写分离主要应用场景是读多写少的场景。比如电商(一般商品浏览请求大于下单请求)、视频平台(浏览请求大于评论)、小说平台(阅读、浏览请求大于发表书评请求)

怎么进行读写分离?

读写分离一般架构设计见下图:

偷两大佬的图

书城模型

怎么避免主从复制延迟?

  • 数据冗余,在接口中提供冗余数据,避免因为数据同步需求再次查询主库

  • 旁路缓存(常见方式是在从库前架个redis ),更新主库后,也更新从库前面的缓存,之后再异步更新从库

  • 关键业务直接查询主库、不做读写分离

什么场景下需要分表分库?

先明确概念:

  • 分表: 把一张大表的数据拆分到同一数据库的多张表中

  • 分库: 把数据拆分不同的数据库中去

分库分表是为了解决两个问题:

  • 表中数据量太大,更新查询慢。可以根据业务特性做分表操作,只读部分数据可以通过查询换粗和主从分离解决,因为表中数据量过大,导致CURD 操作比较耗时,可以通过分表来减少数据查询总量,从而加速CURD。比如一些日志管理系统。

  • 并发量高,单库压力过大, 此时就可以分表,比如秒杀活动。

既分表又分库

  • 既需要解决高并发问题,又需要解决数据量大的问题。比如电商,业务发展到一定阶段之后的架构演变就会涉及到同时分库分表

分表解决什么问题?分库解决什么问题?

见上面回答

分表分库产生什么问题?

分表之后产生的问题

  • 单表可做的全表扫描,分表之后得重新实现

  • 单表可做的全局排序,分表之后得从新实现

  • 单表可做的全局分页查询,分表之后得重新实现

  • 对于横向分表,单表字段更新可能得跨表,这个很有可能是分表不合理导致,要尽量避免过多的表连接

分库之后产生的问题

  • 跨库需求实现起来会很麻烦(跨库事务不支持)

  • 多个库实例,数据一致性保障是需要考虑的。

  • 多实例、多机房分库维护成本更高

分库分表有什么样的拆分方式?分片键sharedkey 怎么选择?

分库分表按拆分对象大概有三个方向:

  • 只分表

  • 只分库

  • 分表分库

按维度划分大概有两个维度:

  • 垂直分表分库,简单地将数据分片划分到相同结构的库/表中,主要缓解存储数据量的问题,但也能缓解并发压力大的问题

  • 水平分表分库,将多列大表,拆分成两个或更多个少列表,将库中多张表拆分成两个或者更多的子库

分片键sharedkey 选择

分片键sharedkey 其实就是分表之后新数据进来后按依据来如入库入表的依据。

sharedkey 选择需要注意以下几个问题

  • 尽量避免数据倾斜(分表之后数据分布在各个表是均匀的)

    最好是具有递增趋势的 - 排序需求

  • 最好是连续的

  • 尽可能的短

sharedkey 选择不当带来的数据倾斜

假设现在有一个电商场景需要做分表分库,有一个数据量很大的订单表有待拆分,表中有卖家,买家数据。我们改按那侧去分表呢?

  • 按卖家分表

如果按卖家分表的话,就会出现数据倾斜的问题,举个例子发,电商中有若干个店铺,总会有几个头部店铺订单量特别大的,总会走许多订单量很少的,这是后如果按卖家分表就会出现头部卖家的订单表特别大,尾部店铺的表数据非常少,头部卖家在不经优化的CURD 操作还是因为海量数据变慢,并没有因为分表分库而带来任何优化。

  • 按买家侧分表

综上看,似乎按买家分表似乎更更合适,之上目前来看不会出现资源倾斜的问题。那么按买家分表,该如何选择分表算法呢?且看下节.

  • 如果上面都不考虑,按月分表呢?

那也会出现数据倾斜,比如记录电商大促的时间段的表数据量就会特别大,淡季的表就比平时更小。

分表算法选择

目前主要的分表算法有几个(这个决定了记录加入到那张表):

  • 直接取模(round robin): 假设有128 张表,用记录中sharedkey % 128 得到的余数就是 入库号/表号

  • hash 取模 :用记录中sharedkey 代入选择好的哈希算法所映射的库/表 就是入库的目标

  • 查表法: 人工划分,将划分关系独立存在一个表中,通过查表入库

  • 一致性 hash:

前两者都有一个缺陷,就是不可扩展,因为一旦加入新库/新表,就需要手动调整 取模函数,哈希函数,还会涉及到数据迁移。

一致下性哈希就是为了解决扩容问题。计算sharedkey 的哈希码 = hashCode(sharedkey) 再将 hashCode(sharedkey) % 2^32 取余,余数落在那个表/数据库上就再那个数据库上加入数据,如果没有命中目标则顺着逆时针找到第一目标进行入库.

分表分库后,如何保证记录ID 全局唯一?

分表分库之后就不同于单表通过唯一性约束就可比保证全局ID 唯一。这是后就需要考虑如何保证新进来的数据是整个系统全局唯一的。主流的全局ID 生成方案有下面三种:

1411012

UUID

通用唯一识别码,IETF 有对应的规范,由一组32位长度的16进制的字符串组成,格式 8-4-4-12 -> 84412

优点:

  • 足够随机,重复概率70亿分之一

缺点:

  • 足够随机,作为Innodb 等树形关系型数据库来说,插入性能很差

  • 无法排序,不能做范围查询

  • 32位可能有点长

基于多表 + 步长做自增主键

优点:

  • 易实现

  • 可排序,可范围查询

  • 长度可定义

缺点:

  • 不易扩容,扩容需要重新规划步长,还有可能需要迁移数据

雪花算法

格式1(符号位)-41(时间戳)-10(机器序列号)-12(业务序列号) -> 1411012

优点:

  • 高性能

  • 基于时间戳,天然有序,可做范围查找

  • 长度8字节

缺点:

  • 高度依赖系统时间,系统始终发生回拨会出现重复发号,该缺陷需要开发人员处理

基于时间生成ID方案发生时间回拨怎么办?

Leaf——美团点评分布式ID生成系统 - 美团技术团队

  • 这个可以参考美团 leaf,发号进程再本地存储最后发出的snowID, 每次发号就批量申请一批ID,拿当前申请的第一个ID 和最后发出的ID 做差如果发现当前发出ID 小于记录ID说明发生了始终回拨,若二者做差小于可接受阈值则告警继续发出,并更新本地记录最后发出ID,否则等待两倍阈值时间,若还是大于阈值时间则告警停止发号服务。

MyCAT 数据库代理中间件

MyCat 一个实现了标准MySQL 协议的数据库代理服务,提供自动分表分库,数据代理服务。

什么时候用到分布式锁?有哪些方案?

多个进程实例争抢同一批资源的时候就用到分布式锁,最常见的场景是多台分布在不同机房的服务实例需要访问某个服务的一批资源,这是后就需要加分布式锁,避免公共资源在使用过程中被其他实例篡改。

分布式锁大概有三种实现方案:

  • 基于数据库引擎支持的锁做的分布式锁,比如innodb, select ... for update 当前读.

  • zookeeper 中间件提供的分布式锁

  • 基于 redis SETNX(key,value) SET key value EX 10 NX 的分布式锁

怎么用redis 实现分布式锁?

基于redis 设计分布式锁就四要素:

  • 锁所有者:一个全局唯一ID 做key,保证锁的申请和释放时同一个实例,不被其他实例释放或者窜改

  • 防死锁:用EX(s)/EP (ms)设置key-val 对自动过期时间

  • 锁名称:便于辨识,在 set key values ex/ep nx中value设置

  • 返回值:通过setnx 接口设置 key-val 对 时的返回值:

    • 1:加锁成功

    • 0:加锁失败,说明有其他实例正在持有该锁

基于Redis 实现分布式锁有那些缺陷?

  • 防锁死锁措施强依赖于过期时间,如果持有实例中间出现GC或其他故障,可能会导致锁自动释放(释放异常)。

Zookeeper 下的分布式锁

在开始介绍zookeeper 的分布式锁前,先介绍相关概念

zookeeper 下的阶段类型

zookeeper 事件类型

zookeeper 实现分布式锁其实靠的是串行化实现的大致工作原理是这样的:

服务会在zookeeper 对应服务根目录下创建一个临时顺序节点,表示要获取资源,zookeeper 作为分布式服务协调者,会取比较对每个资源服务节点下临时节点,若此时公共资源没有被占据,则由序列号最小的节点上锁获取资源,后面的节点会注册watch 服务监听前一节点锁释放事件,当持有资源的节点操作完毕,该节点就会释放锁,删除自身节点(或者当持有锁的节点断开连接,该节点也会被删除,锁也会被释放),此时释放节点的下一个节点就会监听到锁释放事件,这是释放资源节点的下一个节点就会成为当前资源服务节点下序列号最小的节点,这个节点获取资源.... 套娃上面的...

基于 redis 实现的分布式锁 和 zookeeper 实现的分布式锁 对比

基于MySQL 实现的分布式锁

基于MySQL 实现的分布式锁,其实是由引擎事务隔离级别保障的。

比如将事务隔离级别设置位 可重复读、串行时,MySQL 客户端在执行客户端请求的事务时就会加上对应的表锁,记录锁,间隙锁,以保障事务能争取执行。此时并发请求只能等待持有锁的事务执行完成才能重新开始尝试获取锁,在等待过程中是被阻塞的,因此这一机制也可以被用来实现分布式锁。