sql
mysql优化你是怎么做的?
设计数据库时一定要考虑三大范式(确保每一列都保持原子性、确保表中的每一列都和主键相关、确保每一列都和主键列直接相关,而不是间接相关)
mysql的行级锁,表级锁
行级锁是mysql中锁定粒度最细的一种锁,表示只对当前的行进行加锁。行级锁能大大减少数据库操作的冲突。行级锁分为 共享锁和排他锁
特点:开销大,加锁慢,会出现死锁;锁定粒度最小,发生锁冲突的概率最小,并发度也最高
表级锁是mysql中锁定粒度最大的一种锁,表示对当前操作的整张表加锁。最常使用的 MyISAM 与 InnoDB 都支持表级锁定。表级锁定分为 表共享读锁(共享锁)与 表独占写锁(排他锁)
特点:开销小,加锁快,不会出现死锁;锁定粒度大,发生锁冲突的概率最大,并发度最低
页级锁是mysql中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级锁冲突少,但速度慢。因此,采取了折中的页级锁,一次锁定相邻的一组记录。
特点:开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
在 innodb 引擎中既支持行级锁也支持表级锁,那么什么时候会锁住整张表?什么时候只锁住一行呢?
innodb 行级锁是通过给索引上的 索引项 加锁来实现,这种特点意味着:在innodb中只有通过索引条件(不论是主键索引,非主键索引还是普通索引)检索数据时,innodb才会使用行级锁,否则innodb将使用表级锁
在mysql中,行级锁并不是直接锁定记录,而是锁定索引。索引分为主键索引和非主键索引两种。如果一条sql语句操作了主键索引,mysql就会锁定这条主键索引;如果一条sql语句操作了非主键索引,mysql就会先 锁定该 非主键索引,再锁定相关的主键索引;在进行update,delete操作时,mysql不仅锁定where条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的next-key locking
当两个事物同时执行,一个锁住了主键索引,在等待其它相关索引;另一个锁定了非主键索引,在等待主键索引。这样就会发生死锁。发生死锁后,innodb一般都可以检测到,并使一个事物释放锁回退,另一个获取锁完成事物。
mysql避免死锁的方法
有多种方法可以避免死锁,这里只介绍常见的三种:
- 如果不同程序会并发存取多张表,尽量约定以相同的顺序访问表,可以大大降低发生死锁的可能性
- 在同一个事物中,尽可能做到一次锁定所需要的 所有资源,减少死锁产生概率
- 对于非常容易发生死锁的业务,可以尝试使用升级锁定颗粒度,比如通过表级锁来减少死锁产生的概率
内存溢出的解决方法
引起内存溢出的原因有多种,常见的有以下几种:
- 内存中加载的数据量过于庞大,如一次性从数据库中取出过多数据;
- 集合类中有对 对象的引用,使用完以后未清空,使得JVM不能自动回收;
- 代码中存在死循环或错误的递归或循环产生过多重复的对象实体(这一类属于代码层错误);
- 项目中引入的第三方依赖有BUG;
- JVM启动参数内存值设定的过小。
解决方案
第一步:修改JVM启动参数,直接增加内存空间(-Xms,-Xmx参数一定不要忘记加)。
第二步:检查错误日志,查看 OutOfMemory
错误前是否有其它异常或错误。线上的话,可以使用Dump下来分析
第三步:对代码进行分析,找出可能发生内存溢出的位置。
第四步:使用内存查看工具动态查看内存使用情况。某个项目上线后,每次启动系统两天后,就会出现内存溢出的错误,这种情况一般都是代码中出现了缓慢的内存泄漏。这时就要用到内存查看工具了,比如 JProbe Profiler
,Jconsole
等
sql慢查询处理方法
首先如何判别系统遇到了sql慢查询问题?从以下几个方面考虑:
- 数据库CPU负载过高,一般是查询语句中有很多计算逻辑,导致数据库cpu负载过高
- IO负载高导致服务器卡住,这个一般和全表查询 没有索引 有关系
- 查询语句正常,索引正常但是确实慢。如果表的索引是正常的,但是查询慢,这时候需要看看是否 索引没有生效(explain一下)
如果出现了上述问题,解决办法:
开启sql慢查询的日志,要开启日志,需要在mysql的配置文件my.cnf的 [mysqld]
项下配置慢查询日志开启,如下:
|
|
索引失效的情况
- 如果查询条件中
or
,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)
注意:要想使用or,又想要让索引生效,只能将or条件中的每个列都加上索引
- 对于多列索引,不是使用的第一部分(第一个),则不会使用索引(参考:你不知道的mysql多列索引的建立和优化)
like
查询是以%
开头的
- 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引
- 如果mysql估计使用全表扫描要比使用索引快,则不使用索引
此外,查看索引的使用情况命令如下:
show status like 'Handler_read%'
大家可以注意:
handler_read_key:这个值越高越好,越高表示使用索引查询到的次数越多
handler_read_rnd_next:这个值越高,说明查询低效
为什么要分库分表
首先来说一下为什么要分库分表:数据库出现性能瓶颈,用大白话来说就是数据库快扛不住了。
数据库出现性能瓶颈,对外表现有几个方面:
- 大量请求阻塞
在高并发场景下,大量请求都需要操作数据库,导致连接数不够了,请求处于阻塞状态。
- sql 操作变慢
如果数据库中存在一张上亿数据量的表,一条sql没有命中索引会全表扫描,这个查询耗时会非常久
- 存储出现问题
业务量剧增,单库数据量越来越大,给存储造成巨大压力
sql 调优往往是解决数据库问题的第一步,往往投入少部分精力就能获得较大的收益。sql调优的主要目的是尽可能地让那些慢sql变快,手段其实也很简单就是让sql执行尽量命中索引。
开启慢sql记录
如果你使用地是mysql,需要在mysql配置文件中配置几个参数即可。
|
|
调优的工具
常常会用到explain这个命令来查看sql语句的执行计划,通过观察执行结果很容易就知道该sql语句是不是全表扫描,有没有命中索引
|
|
可以看到返回有有一列叫“type”,常见取值有:ALL、index、range、 ref、eq_ref、const、system、NULL(从左到右,性能从差到好)
ALL代表这条sql语句全表扫描了,需要优化。一般来说需要达到range级别及以上
表结构优化
以一个场景举例说明:
“user”表中有 user_id、nickname 等字段,“order”表中有order_id、user_id等字段,如果想拿到用户昵称怎么办?一般情况是通过 join 关联表操作,在查询订单表时关联查询用户表,从而获取导用户昵称。
但是随着业务量的增加,订单表和用户表肯定也是暴增,这时候通过两个表关联数据就比较费力了,为了取一个昵称字段而不得不关联查询几十上百万的用户表,其速度可想而知!
这个时候可以尝试将nickname这个字段加到order表中(order_id、user_id、nickname),这种做法通常叫做数据库表冗余字段。这样做的好处是展示订单列表时不需要再关联查询用户表了
冗余字段的做法也有一个弊端,如果这个字段更新会同时涉及到多个表的更新,因此在选择冗余字段时要尽量选择不经常更新的字段。
架构优化
当单台数据库实例扛不住,我们可以增加实例组成集群对外服务
当发现读请求明显多于写请求时,我们可以让 主实例负责写,从实例 对外提供读的能力
如果读实例压力依然很大,可以在数据库前面加入缓存如redis,让请求优先从缓存取数据减少数据库访问
缓存分担了部分压力后,数据库依然是瓶颈时,这个时候就可以考虑分库分表的方案了。后面会详细介绍
硬件优化
硬件成本非常高,一般来说不可能遇到数据库性能瓶颈问题就去升级硬件
在前期业务量比较小的时候,升级硬件数据库性能可以得到比较大的提升,但是在后期,升级硬件得到的性能提升就不那么明显了
分库分表详解
分库
下面我们以一个商城系统为例逐步说明数据库是如何一步一步演进
单应用数据库
在早期创业阶段想做一个商城系统,基本就是一个系统包含多个基础功能模块,最后打包成一个war包部署,这就是典型的单体架构应用。
如上图,商城系统包括主页Portal模块,用户模块,订单模块,库存模块等,所有的模块都共有一个数据库,通常数据库中有非常多的表。
因为用户量不大,这样的架构在早期完全适用。
多应用但数据库
在前期为了抢占市场,这一套系统就不停的迭代更新,代码量越来越大,架构也变得越来越臃肿,现在随着系统访问压力逐渐增加,系统拆分就势在必行了。
为了保证业务平滑,系统架构的重构也是分了几个阶段进行。
第一个阶段将商城系统单体架构按照功能模块拆分为子服务,比如:Portal服务、用户服务、订单服务、库存服务等。
如上图,多个服务共享一个数据库,这样做的目的是底层数据库访问逻辑可以不用动,将影响降到最低。
多应用多数据库
随着业务推广力度加大,数据库终于成为了瓶颈,这个时候多个服务共享一个数据库基本不可行了。我们需要将每个服务相关的表拆分出来单独建立一个数据库,这其实就是“分库”了。
单数据库能够支撑的并发量是有限的,拆分成多个库可以使服务间不用竞争,提升服务的性能。
如上图,从一个大的数据库中分出多个小的数据库,每个服务都对应一个数据库,这就是系统发展到一定阶段必须要做的“分库”操作
现在非常火的微服务架构也是一样的,如果只拆分应用不拆分数据库,不能解决根本问题,整个系统也很容易达到瓶颈。
分表
说完了分库,那什么时候分表呢?
如果系统处于高速发展阶段,拿商城系统来说,一天下单量可能几十万,那数据库中的订单表增长就特别快,增长到一定阶段数据库查询效率就会出现明显下降。
因此,当单表数据增量过快,业界流传是超过500万的数据量就要考虑分表了。当然500万只是一个经验值,具体根据实际情况来做出决策。
那如何分表呢?
分表有几个维度,一是*水平切分和垂直切分*,二是*单库内分表和多库内分表*
水平拆分和垂直拆分
就拿用户表(user)来说,表中有7个字段:id,name,age,sex,nickname,description
,如果nickname和description不常用,我们可以将其拆分为另外一张表:用户详细信息表,这样就由一张用户表拆分为了用户基本信息表+用户详细信息表,两张表结构不一样相互独立。但是从这个角度来看垂直拆分并没有从根本上解决单表数据量过大的问题,因此我们还需要做一次水平拆分。
还有一种拆分方法,比如表中有一万条数据,我们拆分为两张表,id为奇数的:1,3,5,7.。。。放在user1表中,id为偶数的:2,4,6,8.。。。放在user2中,这样的拆分办法就是水平拆分了。
水平拆分的方式很多,除了上面说的按照id拆表,还可以按照时间维度去拆分,比如订单表,可以按照每日,每月等进行拆分。
- 每日表:只存储当天的数据
- 每月表:可以起一个定时任务将前一天的数据全部迁移到当月表
- 历史表:同样可以用定时任务把时间超过30天的数据迁移到 history 表
总结一下水平拆分和垂直拆分的特点:
- 垂直拆分:基于表或字段拆分,表结构不同
- 水平拆分:基于数据拆分,表结构相同,数据不同。
单库内拆分和多库内拆分
拿水平拆分为例,每张表都拆分为了多个子表,多个子表存在于同一数据库中。比如下面用户表拆分为用户表1和用户表2.
在一个数据库中将一张表拆分为几个子表在一定程度上可以解决单表查询性能的问题,但是也会遇到一个问题:单数据库存储瓶颈。
所以在业界用的更多的还是将子表拆分到多个数据库中。比如下图中,用户表拆分为两个子表,两个子表分别存在于不同的数据库中。
一句话总结:分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。
分库分表带来的复杂性
既然分库分表这么好,那我们是不是在项目初期就应该采用这种方案呢?不要激动,冷静一下,分库分表的确解决了很多问题,但是也给系统带来了很多复杂性,下面简要地谈一谈。
(1)跨库关联查询
在单库未拆分表之前,我们可以很方便的使用 join 操作关联多张表查询数据,但是经过分库分表之后两张表可能都不在一个数据库中,如何使用 join 呢?
有几种方案可以解决:
- 字段冗余:把需要关联的字段放入主表中,避免join操作;
- 数据抽象:通过 ETL 等将数据汇合聚集,生成新的表;
- 全局表:比如一些基础表可以在每个数据库中都放一份;
- 应用层组装:将基础数据查出来,通过应用程序计算组装。
(2)分布式事物
单数据库可以用本地事务搞定,使用多数据库就只能通过分布式事物解决了。
常用的解决方案有:基于可靠消息(MQ)的解决方案、两阶段事物提交、柔性事物等。
(3)排序、分页、函数计算问题
在使用sql的 order by,limit等关键字需要特殊处理,一般来说采用分片的思路:
先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终得到结果。
(4)分布式ID
如果使用mysql数据库在单库单表里面可以使用id自增作为主键,分库分表了之后就不行了,会出现id重复。
常用的分布式ID解决方案有:
- uuid
- 基于数据库自增单独维护一张 ID 表
- 号段模式
- Redis缓存
- 雪花算法(SnowFlake)
- 百度 uid-generator
- 美团 Leaf
- 滴滴 Tinyid
(5)多数据源
分库分表之后可能会面临从 多个数据库或多个子表中获取数据,一般的解决思路有:客户端适配和代理层适配。
业界常用的中间件有:
- shardingsphere(前身 sharding-jdbc)
- Mycat
Redis
Redis 的淘汰策略
将redis作为缓存使用时,当redis内存超出物理内存限制时,内存数据就会与磁盘产生频繁交换,导致redis性能急剧下降。此时如何淘汰无用数据释放空间,就很重要了。
redis在生产环境中,采用配置参数 maxmemory
的方式来限制内存大小,当实际内存超出这个值时,通过以下几种方式淘汰:
volatile-lru
:从设置过期时间的数据集(server.db[i].expires)中挑选出最近最少使用的数据淘汰。没有设置过期时间的key不会被淘汰,这样就可以在增加内存空间的同时保证需要持久化的数据不会丢失。volatile-ttl
:除了淘汰机制采用LRU,策略基本上与volatile-lru相似,从设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰,ttl值越大越优先被淘汰。volatile-random
:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。当内存达到限制无法写入非过期时间的数据集时,可以通过该淘汰策略在主键空间中随机移除某个key。allkeys-lru
:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰,该策略要淘汰的key面向的是全体key集合,而非过期的key集合。allkeys-random
:从数据集(server.db[i].dict)中选择任意数据淘汰。no-enviction
:禁止驱逐数据,也就是当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行,线上任务也不能持续进行,采用no-enviction策略可以保证数据不被丢失,这也是系统默认的一种淘汰策略。
上述是Redis的6种淘汰策略,关于使用这6种策略,开发者还需要根据自身系统特征,正确选择或修改驱逐。
- 在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。
- 如果所有数据访问概率大致相等时,可以选择allkeys-random。
- 如果研发者需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。
- 如果希望一些数据能长期被保存,而一些数据可以被淘汰掉时,选择volatile-lru或volatile-random都是比较不错的。
- 由于设置expire会消耗额外的内存,如果计划避免Redis内存在此项上的浪费,可以选用allkeys-lru 策略,这样就可以不再设置过期时间,高效利用内存了。
如何保证缓存数据一致
只要使用到缓存,无论是本地内存做缓存还是使用 redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。接下来就讨论一下关于保证缓存和数据库双写时的数据一致性。
解决方案
那么我们这里列出来所有策略,并且讨论他们优劣性。
- 先更新数据库,后更新缓存
- 先更新数据库,后删除缓存
- 先更新缓存,后更新数据库
- 先删除缓存,后更新数据库(个人建议这种,利用延时删除的策略)
先更新数据库,后删除缓存
此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:
- 请求 A 先对数据库进行更新操作
- 在对 Redis 进行删除操作的时候发现报错,删除失败
- 此时将Redis 的 key 作为消息体发送到消息队列中
- 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作
但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
Zookeeper
Zookeeper是一个开源的分布式协调服务,它的目标是可以提供高性能、高可用和顺序访问控制的能力,同时也是为了解决分布式环境下数据一致性的问题。
集群
首先,Zookeeper集群中有几个关键的概念,Leader、Follower和Observer,Zookeeper中通常只有Leader节点可以写入,Follower和Observer都只是负责读,但是Follower会参与节点的选举和过半写成功,Observer则不会,它只是单纯的提供读取数据的功能。
通常这样设置的话,是为了避免太多的节点参与过半写的过程,导致影响性能,这样Zookeeper只要使用一个几台机器的小集群就可以实现高性能了,如果要横向扩展的话,只需要增加Observer节点即可。
Zookeeper建议集群节点个数为奇数,只要超过一般的机器能够正常提供服务,那么整个集群都是可用的状态。
数据节点Znode
Zookeeper中数据存储于内存之中,这个数据节点就叫做Znode,它是一个树形结构,比如/a/b/c类似。
而Znode又分为持久节点、临时节点、顺序节点三大类。
持久节点是指只要被创建,除非主动移除,否则都应该一直保持在Zookeeper中。
临时节点不同的是,它的声明周期和客户端Session会话一样,会话失效,那么临时节点就会被移除。
还有就是临时顺序节点和持久顺序节点,除了基本的特性之外,子节点的名称还是有有序性。
会话Session
会话自然就是指Zookeeper客户端和服务端之间的通信,它们使用TCP长连接的方式保持通信,通常,肯定会有心跳检测的机制,同时它可以接受来自服务器的watch事件通知。
事件监听器Watcher
用户可以在指定的节点上注册watcher,这样在事件触发的时候,客户端就会收到来自服务端的通知。
权限控制 ACL
Zookeeper使用 acl 来进行权限的控制,包含以下五种:
- create,创建子节点权限
- delete,删除子节点权限
- read,获取节点数据和子节点列表权限
- write,更新节点权限
- admin,设置节点 ACL 权限
所以,Zookeeper通过集群的方式来做到高可用,通过内存数据节点Znode来达到高性能,但是存储的数据量不能太大,通常使用于读多写少的场景。
Zookeeper有哪些应用场景
- 命名服务Name Service,依赖Zookeeper可以生成全局唯一的节点id,来对分布式系统中的资源进行管理。
- 分布式协调,这是Zookeeper的核使用了。利用watcher的监听机制,一个系统的某个节点状态发生改变,另外系统可以得到通知。
- 集群管理,分布式集群中状态的监控和管理,使用Zookeeper来存储。
- Master选举,利用Zookeeper节点的全局唯一性,同时只有一个客户端能够创建成功的特点,可以作为Master选举使用,创建成功的则作为Master。
- 分布式锁,利用Zookeeper创建临时顺序节点的特性。
说说watcher监听机制和它的原理?
Zookeeper可以提供分布式数据的发布/订阅功能,依赖的就是watcher监听机制。
客户端可以向服务端注册watcher监听,服务端的指定事件触发之后,就会向客户端发送一个事件通知。
它有几个特性:
- 一次性,一旦一个watcher触发之后,Zookeeper就会将它从存储中移除。
- 客户端串行,客户端的watcher回调处理是串行同步的过程,不要因为一个watcher的逻辑阻塞整个客户端。
- 轻量,watcher通知的单位是watchedEvent,只包含通知状态、事件类型和节点类型,不包含具体的事件内容,具体的事件内容需要客户端主动去重新获取数据。
主要流程如下:
- 客户端向服务端注册watcher监听
- 保存watcher对象到客户端本地的watcherManager中
- 服务端watcher事件触发后,客户端收到服务端通知,从watcherManager中取出对应的watcher对象执行回调逻辑。
Zookeeper是如何保证数据一致性的
Zookeeper通过ZAB原子广播协议来实现数据的最终顺序一致性,他是一个类似2PC两阶段提交的过程。
由于Zookeeper只有Leader节点可以写入数据,如果是其他节点收到写入数据的请求,则会将之转发给Leader节点。
主要流程如下:
- Leader收到请求之后,将它转换为一个proposal提议,并且为每个提议分配一个全局唯一递增的事务ID:zxid,然后把提议放入到一个FIFO的队列中,按照FIFO的策略发送给所有的Follower
- Follower收到提议之后,以事务日志的形式写入到本地磁盘中,写入成功后返回ACK给Leader
- Leader在收到超过半数的Follower的ACK之后,即可认为数据写入成功,就会发送commit命令给Follower告诉他们可以提交proposal了
ZAB包含两种基本模式,崩溃恢复和消息广播。
整个集群服务在启动、网络中断或者重启等异常情况的时候,首先会进入到崩溃恢复状态,此时会通过选举产生Leader节点,当集群过半的节点都和Leader状态同步之后,ZAB就会退出恢复模式。之后,就会进入消息广播的模式。
Zookeeper如何进行Leader选举的?
Leader的选举可以分为两个方面,同时选举主要包含事务zxid和myid,节点主要包含LEADING\FOLLOWING\LOOKING3个状态。
- 服务启动期间的选举
- 服务运行期间的选举
服务启动期间的选举
- 首先,每个节点都会对自己进行投票,然后把投票信息广播给集群中的其他节点
- 节点接收到其他节点的投票信息,然后和自己的投票进行比较,首先zxid较大的优先,如果zxid相同那么则会去选择myid更大者,此时大家都是LOOKING的状态
- 投票完成之后,开始统计投票信息,如果集群中过半的机器都选择了某个节点机器作为leader,那么选举结束
- 最后,更新各个节点的状态,leader改为LEADING状态,follower改为FOLLOWING状态
服务运行期间的选举
如果开始选举出来的leader节点宕机了,那么运行期间就会重新进行leader的选举。
- leader宕机之后,非observer节点都会把自己的状态修改为LOOKING状态,然后重新进入选举流程
- 生成投票信息(myid,zxid),同样,第一轮的投票大家都会把票投给自己,然后把投票信息广播出去
- 接下来的流程和上面的选举是一样的,都会优先以zxid,然后选择myid,最后统计投票信息,修改节点状态,选举结束
那选举之后又是怎样进行数据同步的?
那实际上Zookeeper在选举之后,Follower和Observer(统称为Learner)就会去向Leader注册,然后就会开始数据同步的过程。
数据同步包含3个主要值和4种形式。
PeerLastZxid:Learner服务器最后处理的ZXID
minCommittedLog:Leader提议缓存队列中最小ZXID
maxCommittedLog:Leader提议缓存队列中最大ZXID
直接差异化同步 DIFF同步
如果PeerLastZxid在minCommittedLog和maxCommittedLog之间,那么则说明Learner服务器还没有完全同步最新的数据。
- 首先Leader向Learner发送DIFF指令,代表开始差异化同步,然后把差异数据(从PeerLastZxid到maxCommittedLog之间的数据)提议proposal发送给Learner
- 发送完成之后发送一个NEWLEADER命令给Learner,同时Learner返回ACK表示已经完成了同步
- 接着等待集群中过半的Learner响应了ACK之后,就发送一个UPTODATE命令,Learner返回ACK,同步流程结束
先回滚再差异化同步 TRUNC+DIFF同步
这个设置针对的是一个异常的场景。
如果Leader刚生成一个proposal,还没有来得及发送出去,此时Leader宕机,重新选举之后作为Follower,但是新的Leader没有这个proposal数据。
举个栗子:
假设现在的Leader是A,minCommittedLog=1,maxCommittedLog=3,刚好生成的一个proposal的ZXID=4,然后挂了。
重新选举出来的Leader是B,B之后又处理了2个提议,然后minCommittedLog=1,maxCommittedLog=5。
这时候A的PeerLastZxid=4,在(1,5)之间。
那么这一条只存在于A的提议怎么处理?
A要进行事务回滚,相当于抛弃这条数据,并且回滚到最接近于PeerLastZxid的事务,对于A来说,也就是PeerLastZxid=3。
流程和DIFF一致,只是会先发送一个TRUNC命令,然后再执行差异化DIFF同步。
仅回滚同步 TRUNC同步
针对PeerLastZxid大于maxCommittedLog的场景,流程和上述一致,事务将会被回滚到maxCommittedLog的记录。
这个其实就更简单了,也就是你可以认为TRUNC+DIFF中的例子,新的Leader B没有处理提议,所以B中minCommittedLog=1,maxCommittedLog=3。
所以A的PeerLastZxid=4就会大于maxCommittedLog了,也就是A只需要回滚就行了,不需要执行差异化同步DIFF了。
全量同步 SNAP同步
适用于两个场景:
- PeerLastZxid小于minCommittedLog
- Leader服务器上没有提议缓存队列,并且PeerLastZxid不等于Leader的最大ZXID
这两种场景下,Leader将会发送SNAP命令,把全量的数据都发送给Learner进行同步。
有可能会出现数据不一致的问题吗?
还是会存在的,我们可以分成3个场景来描述这个问题。
查询不一致
因为Zookeeper是过半成功即代表成功,假设我们有5个节点,如果123节点写入成功,如果这时候请求访问到4或者5节点,那么有可能读取不到数据,因为可能数据还没有同步到4、5节点中,也可以认为这算是数据不一致的问题。
解决方案可以在读取前使用sync命令。
leader未发送proposal宕机
这也就是数据同步说过的问题。
leader刚生成一个proposal,还没有来得及发送出去,此时leader宕机,重新选举之后作为follower,但是新的leader没有这个proposal。
这种场景下的日志将会被丢弃。
leader发送proposal成功,发送commit前宕机
如果发送proposal成功了,但是在将要发送commit命令前宕机了,如果重新进行选举,还是会选择zxid最大的节点作为leader,因此,这个日志并不会被丢弃,会在选举出leader之后重新同步到其他节点当中。
如果作为注册中心,Zookeeper 和Eureka、Consul、Nacos有什么区别?
Nacos | Eureka | Consul | Zookeeper | |
---|---|---|---|---|
一致性协议 | CP+AP | AP | CP | CP |
健康检查 | TCP/HTTP/MYSQL/Client Beat | Client Beat | TCP/HTTP/gRPC/Cmd | Keep Alive |
负载均衡策略 | 权重/ metadata/Selector | Ribbon | Fabio | — |
雪崩保护 | 有 | 有 | 无 | 无 |
自动注销实例 | 支持 | 支持 | 不支持 | 支持 |
访问协议 | HTTP/DNS | HTTP | HTTP/DNS | TCP |
监听支持 | 支持 | 支持 | 支持 | 支持 |
多数据中心 | 支持 | 支持 | 支持 | 不支持 |
跨注册中心同步 | 支持 | 不支持 | 支持 | 不支持 |
SpringCloud集成 | 支持 | 支持 | 支持 | 不支持 |
Dubbo集成 | 支持 | 不支持 | 不支持 | 支持 |
K8S集成 | 支持 | 不支持 | 支持 | 不支持 |
最后,你对于CAP理论怎么理解?
CAP是一个分布式系统设计的定理,他包含3个部分,并且最多只能同时满足其中两个。
- Consistency一致性,因为在一个分布式系统中,数据肯定需要在不同的节点之间进行同步,就比如Zookeeper,所以一致性就是指的是数据在不同的节点之间怎样保证一致性,对于纯理论的C而言,默认的规则是忽略掉延迟的,因为如果考虑延迟的话,因为数据同步的过程无论如何都会有延迟的,延迟的过程必然会带来数据的不一致。
- Availability可用性,这个指的是对于每一个请求,节点总是可以在合理的时间返回合理的响应,比如Zookeeper在进行数据同步时,无法对外提供读写服务,不满足可用性要求。这里常有的一个例子是说Zookeeper选举期间无法提供服务不满足A,这个说法并不准确,因为CAP关注的是数据的读写,选举可以认为不在考虑范围之内。所以,可以认为对于数据的读写,无论响应超时还是返回异常都可以认为是不满足A。
- Partition-tolerance分区容错性,因为在一个分布式系统当中,很有可能由于部分节点的网络问题导致整个集群之间的网络不连通,所以就产生了网络分区,整个集群的环境被分隔成不同的的子网,所以,一般说网络不可能100%的不产生问题,所以P一定会存在。
为什么只能同时满足CAP中的两个呢?
以A\B两个节点同步数据举例,由于P的存在,那么可能AB同步数据出现问题。
如果选择AP,由于A的数据未能正确同步到B,所以AB数据不一致,无法满足C。
如果选择CP,那么B就不能提供服务,就无法满足A。