1. 数据库及数据相关中间件
1.1. 简介
报考岗位为 2025年 信息支援部队某单位-数据分析与管理 岗位。
1.2. 详细内容
1.2.1. MySQL相关
ACID
Atomic,原子性;
Consistency,一致性;
Isolation,隔离性;
Durability,持久性;
RR: Repeatable Read(可重复读)
是 MySQL 默认事务隔离级别。在该隔离级别下,事务中的查询会看到事务开始时的一致数据快照,即使其他事务对数据进行了修改,也不会影响当前事务的读取结果。
适用于需要数据一致性和避免不可重复读的场景。它在财务系统、库存管理和报表生成等场景中非常有用,但需要注意幻读问题和性能影响。
白话:有两个事务,第一个事务查询一条数据,第二个事务修改这条数据提交后(出于性能考虑,使用了以乐观锁为理论基础的MVCC<多版本并发控制>来实现),第一个事务又查询该数据,第一个事务前后两次查询的结果不致。
mysql查询流程
系统连接数据库,建立好连接后,系统和mysql都有对应的连接池,当有请求时,从连接池获取连接;
发送要执行的sql,mysql会有线程来处理这个请求,交给SQL接口,然后交给SQL查询解析器(解析SQL打算做什么);
再交给查询优化器(清楚干什么之后,优化传输过来的SQL,在数据库层面更高效的执行);
然后交给执行器(根据执行计划调用数据库底层存储引擎的接口),存储引擎根据接口调用情况,在内存和磁盘进行相关操作后返回结果(很核心的组件)。
BufferPool
缓冲池(默认128M),更新等操作需要先把数据加到缓冲池才能操作,按页加载;
里面存放描述页和缓存的数据页(描述页是对缓存数据页<大小16K>的描述);
多线程并发加锁时,只能有一个线程来操作,串行化执行效率不高,默认如果内存不大于1G,只有一个BufferPool,大于1G可以调整BufferPool数量提升并发量。
chunk机制
每个BufferPool中有多个chunk(默认128M),然后共享free、flush、lru链表,当扩容时申请新的chunk就可以,然后把chunk挂到BufferPool上,不需要申请新的大块内存然后迁移数据(可以运行期间动态的扩容)。
free链表
双向链表,链表中都是空闲的数据页描述数据块的地址,有基础的节点,每个描述页游free_pre和free_next指针记录free链表(加载数据到BufferPool时,从free链表获取一个描述块,从磁盘把数据页读取到BufferPool,然后将描述信息写到描述块中,将该描述块从free链表移除)
数据页缓存哈希表
表空间号+数据页作为key,value是缓存页的地址,如果使用数据页的时候先查询一下,想要找的数据页是否在BufferPool中
flush链表
凡是被修改过的数据页,都会把描述块的地址挂到flush链表中,都是要刷新到磁盘中的,描述块会记录flush链表的前后指针
LRU链表
最近最少使用链表,只要缓存页被使用访问了,就把描述块移动到头部,尾部的就是最久没使用的,如果要移除缓存页(刷新到磁盘),优先选择尾部的缓存页;
简单的LRU会导致问题,因为mysql有预读机制,将目标前后相关的部分页也加载到BufferPool中,如果空间不足会把实际需要的数据删掉;
冷热数据思想(冷数据37%),刚加载的数据放到冷数据的头部,1s之后如果还有访问,就会移动到热数据的头部,热数据中后3/4的被访问才会移动到头部,前1/4的访问不会移动。
undo日志
加载到bufferpool的数据,旧数据会写到undo日志,方便回滚
redo log buffer
当bufferPool的数据修改了,没有落到磁盘,防止系统宕机数据丢失(提交事务后redoLogBuffer通过参数设置会落到磁盘上,如果宕机,启动后再恢复数据)
redo日志
当提交事务时,会将redo log buffer写入磁盘文件,偏物理的,记录的是对哪个数据页的那条数据做了什么修改,innodb特有的(刷盘策略通过innodb_flush_log_at_trx_commit来设置,0提交时不会吧redo log buffer刷入磁盘,1提交时必须把数据刷入磁盘,只要事务成功,数据肯定在磁盘,2提交时会把数据刷入os cache,大概1s才会进入磁盘,最好设置为1);
多个redo log记录写入redo log block,然后把block写到文件中
binlog
归档日志,偏逻辑性的,类似,对某个表中id=1的数据做了什么修改,属于mysql server自己的日志文件,提交事务时会写入binlog(刷盘策略,通过sync_binlog来控制,默认0进入os cache,1是提交事务直接写入磁盘),写完binlog后会把对应文件、位置写入redo log,记录commit标识(有commit标识的才算最终事务成功)
后台IO线程
随机将BufferPool的脏数据刷回磁盘中,此时宕机也不要紧,因为启动时会根据redo log恢复BufferPool的数据;
有后台线程一定时间从LRU冷数据尾部取部分数据刷新到磁盘,然后从LRU和flush链表删除,加到free链表,也会随机从flush链表把数据页刷新到磁盘,然后从flush链表和LRU链表移除,加到free链表。
InnoDB存储引擎
修改数据时,把数据页加载到BufferPool中,然后记录undo日志(可以回滚数据),修改BufferPool的数据,然后写redo log buffer(偏物理的,哪个页的哪条数据修改为什么);
提交事务时会根据参数情况选择刷盘策略,把redo log buffer刷盘,并记录binlog(偏逻辑的,哪张表id=1的数据修改为什么),然后在redo log中记录binlog文件、位置,最后记录一个commit标识,后台会有IO线程,随机将Buffer Pool的脏数据刷回到磁盘中。
事务隔离级别
1、读未提交
两个事务同时操作数据库,其中一个事物修改数据库后,没有提交,另一个事物可以读取到修改过的数据。
2、读已提交
有两个事务,第一个事务查询一条数据,第二个事务修改这条数据提交后,第一个事务又查询该数据,第一个事务前后两次查询的结果不一致。
3、可重复读(mysql默认隔离级别,通过MVCC
有两个事务,第一个事务查询一条数据,第二个事务修改这条数据提交后(出于性能考虑,使用了以乐观锁为理论基础的MVCC<多版本并发控制>来实现),第一个事务又查询该数据,第一个事务前后两次查询的结果不致。
4、串行化(解决 幻读 的问题)
可重复读可以限制两个事务并发修改删除数据导致前后查询不一致的情况,但是无法限制insert(行锁只能锁住行,新增数据无法限制)。
幻读的案例场景:有两个事务,第一个数据查询列表数据条数,第二个事务新增数据到表中并提交事务,第一个事务又重新查询数据条数,第一个事务前后两次查询的数据不一致。
串行化:有两个事务,第一个事务查询数据,第二个事务新增数据到表中,会报错(表级锁),不能插入数据,第一个事务再查询数据,前后两次结果一致。串行化并发低。
undo log版本链
每次更新之前会记录旧数据到undo log中,每条记录都包含txr_id(最近更新这条记录的事务id)和roll_pointer(指向之前生存的undo log记录),多个事务执行完,undo log的记录组成一个链条。
ReadView机制
m_ids(哪些事务还没提交),min_trx_id(m_ids中最小的id),max_trx_id(下一个新事务的id),creator_trx_id(当前事务的id);
查询时只会查询到最新的事务id并且不在m_ids里对应的一条数据。最开始查询到数据trx_id是小于自己事务的,之后并发有更新操作,再查询发现trx_id比自己的大,不能查询出来,顺着roll_pointer向下查找,找到不大于自己事务id的最新数据。
读已提交(RC)实现是,每次查询都新生存一个ReadView,使用事务提交后,新的RV,正在进行的事务不包含提交的事务,此时就可以查询到已经提交的数据。
mvcc机制
基于RV,查询的时候生成,当并发有修改时,正在进行的事务中,获取不大于自己事务id的最新值,已经提交的数据中,查询小于RV最大的事务id的数据,最终结果是这两种情况最新的数据。
索引
将每个页最小主键和页号放在一起组成索引目录
B+树
数据库中最开始数据放在一个页中,当数据量越来越大,就把主见和页号维护在索引页中,当索引页也变大,会再抽离出来新的索引页,这种非叶子节点是索引目录,叶子节点是数据的树形结构就是B+树。
查找数据时,根据主键,在树形结构中层层查找,最终就会定位到叶子节点找到数据。
聚簇索引
B+树中如果叶子节点就是数据页本身,此时称这棵B+树索引为聚簇索引
二级索引
每个二级索引都是一颗B+树,叶子节点存放的是主键+索引字段的值,索引放的是字段及对应的页号,找到数据之后,还需要根据id回表查询完整的数据
联合索引
联合索引和二级索引类似,只是叶子节点放的是多个字段,索引页也是。按照设置字段进行依次排序(等值匹配,最左匹配,最左前缀匹配,范围查找,等值匹配+范围查找)
优化sql
查询建立的索引尽量包含where、order by、group by;
建立索引尽量找基数大的(二分查找有优势),如果字段太长可以取部分前缀建立索引(可以定位以这个前缀的部分数据再回表);
建议一张表2~3个就可以了,太多也会影响性能,建议主键字段,不要UUID这种,会页分裂损耗性能。
核心重点就是,尽量利用一两个复杂的多字段联合索引,抗下你80%以上的 查询,然后用一两个辅助索引抗下剩余20%的非典型查询,保证你99%以上的查询都能充分利用索引,就能保证你的查询速度和性能!
SQL调优的时候,核心就是分析执行计划里哪些地方出现了全表扫描,或者扫描数据过大,尽可能通过合理优化索引保证执行计划每个步骤都可以基于索引执行,避免扫描过多的数据。
执行计划
type,const、ref等类型;const,直接通过索引(保证二级索引每个都是唯一)定位,非常快;
ref,普通二级索引,不是唯一,查询速度也很快;
ref_or_null,在ref基础上有null限定;
index,通过二级索引就可以获取想要的数据就叫index;
prossible keys,有多个索引,可能会用到多个索引;
rows,大概会读取表里的记录数;
data_length,表聚簇索引的字节大小(除以1024代表有多少kb除以16k,代表有多少页);
检测成本是0.2,读取数据页成本是1,计算成本就是数据页数1+数据条数0.2;
多表关联
1、INNER JOIN(内连接):只返回两个表中匹配的行。
2、LEFT JOIN(左连接):返回左表中的所有行,即使右表中没有匹配的行。
3、RIGHT JOIN(右连接):返回右表中的所有行,即使左表中没有匹配的行。
4、FULL OUTER JOIN(全外连接):返回两个表中的所有行,如果没有匹配的行,则用 NULL 填充。 (注意:MySQL 不支持 FULL OUTER JOIN,但可以通过 UNION 实现)
5、CROSS JOIN(交叉连接):返回两个表的笛卡尔积(所有可能的组合)。
1.2.2. redis相关
redis数据类型
(1)String 最简单的数据结构,get/set k v数据;
(2)hash 类似map的数据结构,一般可以将结构化的数据,例如一个对象缓存到redis中,每次读写可以操作一个字段;
(3)list 有序列表,例如可以通过list存储一些列表的数据结构,类似粉丝列表、文章评论之类的数据,可以通过lrange 命令,从某个元素开始读取多少个元素,可以基于list实现分页查询,基于redis实现高性能分页,流式分页,一页一页往下翻;可以做简单的消息队列,从list头进去,从list尾部处理啊;
(4)set 无序集合,自动去重,可以基于set取交集,并集,差集,看看共同好友
(5)sorted set(zset) 排序的set<底层数据结构是跳表>,去重,可以排序,写进去的时候给一个分数,自动根据分数排序,排行榜:将每个用户以及对应的分数写入进去,获取排名
redis数据过期策略
1、设置过期时间
设置数据的时候,可以指定key一个过期时间,例如只能存活1小时,定期删除+惰性删除(定期删除,指的是redis默认每隔100ms随机抽取一些设置了过期时间的key,检查是否过期,如果过期就删除。<不是遍历所有的key来检查删除>;
惰性删除,获取key的时候,检查这个key如果设置了过期时间,判断是否过期,如果过期了此时就会删除)两个手段结合,保证过期的key一定会删除。
2、内存淘汰机制
如果redis内存占用过多的时候,会进行内存淘汰:
1)noeviction:当内存不足以容纳新写入的数据时,新写入的操作会报错(没人用);
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(最常用);
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key(一般没人用);
4)volatile-lru:当内存不足以容纳新写入数据时,在设置过期时间的键空间中,移除最近最少使用的key(一般不太合适);
5)volatile-random:当内存不足以容纳新写入数据时,在设置过期时间的键空间中,随机删除;
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置过期时间的键空间中,有更早过期的key优先删除
3、手写LRU算法
继承LinkedHashMap
构造函数设置数量,最后参数设置为true,让LinkedHashMap按照访问顺序进行排序,最近访问的在前,最老访问的放在后面
重载removeEldestEntry
removeEldestEntry(Map.Entry eldest){
//当map中数据量大于指定的缓存个数时,自动删除最老的数据
return size() > CACHE_SIZE;
}
单线程为什么高效
建立连接后,把请求通过IO多路复用器压入队列,然后文件事件分派器将事件分派到不同处理器去执行,基于纯内存操作,非阻塞的IO多路复用器,单线程避免频繁的切换上下文,所以高效。
持久化
RDB方式和AOF方式;
RDB内存快照,AOF操作回放;
建议两种结合使用
主从
读写分离,一主多从,扛高并发,可以水平扩展;
第一次发送rdb文件(内存中创建不会落磁盘),后面增量发送命令,如果网络断了,再连接后,会从之前offset位置继续发送,slave不会过期,master过期后会发送一个del命令
哨兵
检测节点,实现主从切换,保证集群高可用,不能保证数据0丢失,集群中至少3个节点(n/2+1),选举超过一半的才会被认可;
节点宕机数据会丢失,脑裂数据也会丢失,通过设置异步复制的事件,减少数据丢失,通过设置slave向master发送ack消息减少脑裂导致的数据丢失(如果超过指定时间没收到ack,拒绝写入)
Redis Cluster
集群平分16384个hash slot,通过计算key对应的slot寻找分片
缓存数据双写
先删除缓存数据,再修改数据库;
高并发情况下,可以将一条数据的操作查询串行化
缓存穿透、缓存雪崩及如何预防
1、缓存雪崩:
缓存全面宕机,大流量全都请求到mysql,把数据库打死,重启之后还会被打死
2、缓存穿透:
恶意攻击,数据库和缓存都没有,大量请求永远走不到缓存,每次都是走数据库(查不到数据),导致数据库打死。 (查不到数据,设置一个空信息到缓存)
3、预防:
事前,redis高可用,避免完全崩溃(主从+哨兵,redis cluster);
事中,本地ecache缓存 + hystrix限流&降级,避免Mysql被打死;
事后,redis持久化,快速恢复缓存数据
1.2.3. 分布式事务相关
spring事务
TransactionInterceptor控制,开启事务,执行业务逻辑,判断是否保证,选择提交还是回滚,底层调用的hibernate事务,最底层就是jdbc的方法和api
2pc 两阶段提交
1、2pc 流程
(1) 准备阶段
TM先发送prepare消息给各个数据库,各个数据库先开启事务,执行操作,不进行提交,然后各个数据库返回响应消息给事务管理器;
(2) 提交阶段
第一种情况,发现有数据库执行失败或者没有返回消息(超时),TM认为分布式事务失败,然后通知所有数据库全部回滚;
第二种情况,所有数据库都返回响应,操作成功,TM认为分布式事务成功,然后通知所有数据库提交事务。;
2、2pc 缺陷
在2PC阶段,如果TM挂掉或者数据库网路故障,无法接收commit消息,此时会导致数据库一直锁住资源,别人无法访问操作这个资源
(1) 同步阻塞
在阶段一里执行prepare操作会占用资源,一直到整个分布式事务完成,才会释放资源,这个过程中,其他人如果访问操作数据库,会被阻塞住
(2) 单点故障
TM如果是单机的,如果发生故障,就完蛋了
(3) 事务状态丢失
如果TM是双机热备的,一个TM挂掉了(接收到Commit消息的数据库也挂掉了),自动选举出的其他TM,不知道当前这个分布式事务状态,因为不知道哪个数据库接收过commit
(4) 脑裂问题
在阶段二中,发送commit消息时,有些数据库可以接收到消息,有些因为网络等问题,导致commit消息丢失等,最终会导致多个数据库的数据不同步的
3pc 三阶段提交
1、3pc 流程
(1) CanCommit阶段
TM发送CanCommit消息给各个数据库,然后各个数据库返回结果,不会执行sql,就是看看网络环境(比2pc好)
(2) PreCommit阶段
如果各个数据库CanCommit消息都成功,进入PreCommit阶段,TM发送PreCommit消息给各个库(相当于2PC阶段一),其实就会执行各个SQL语句,只是不提交;
如果有CanCommit消息返回失败,TM发送abort消息给各个库,结束分布式消息。
(3) DoCommit阶段
如果各个库PreCommit阶段都成功了,就会发送DoCommit消息给各个数据库,提交本地事务,分布式事务成功(超时机制,自动提交);
如果有库对PreCommit返回失败,或者超时没返回,TM认为分布式事务失败,直接发送abort消息给各个库,本地事务回滚,分布式回滚
2、3pc 缺陷
如果TM在DoCommit阶段发送abort消息,发生脑裂问题,某个库没收到abort消息,自己提交本地事务,其实还是不对的
3、常见 3pc 实现方式
(1) XA
XA就是定义好TM(事务管理器)与RM(资源管理器,数据库)之间的接口规范,就是管理分布式事务组件跟各个数据库之间的一个接口。用来统一跟各个数据库通信和交互,通知一起提交或者回滚。
(2) JTA+Atomikos
横跨多个事务,通过JTA API来支持的,可以协调和横跨多个数据库的分布式事务,一般会结合JNDI(J2EE老生常谈的一套东西),J2EE里很多东西定义的很好,但业内使用的时候没有多少公司使用。
(3) TCC:Try、Confirm、Cancel
用到了补偿的概念,分为三个阶段:
1)try阶段:这个阶段是对各个服务的资源做检测以及对资源进行锁定或者预留
2)confirm阶段:这个阶段在各个服务中执行实际操作
3)cancel阶段:如果任何一个服务的业务方法执行出错,这里就需要进行补偿,就是已经执行成功的业务逻辑进行回滚
例如:
1)Try阶段:先把两个银行账户中的资金冻结,不让操作
2)Confirm阶段:执行实际的业务转账,A银行账户资金扣减,B银行资金增加
3)Cancel阶段:任何一个步骤失败,就进行补偿回滚(例如,A银行已经扣减,但是B银行增加资金失败,那么把A银行的金额再加回去)
这种方案也比较少用,因为补偿代码量巨大,然后与业务重耦合,不好维护。一般跟钱相关的,一点不允许出错的,可以使用这种方案。
可靠消息最终一致性
不使用消息表了,基于MQ来实现,例如,阿里的RocketMQ(3.2.6之前版本)就支持消息事务
1)A系统先发送一个prepared消息到mq,如果消息发送失败,就直接取消操作不执行
2)如果消息发送成功,执行本地的事务,如果成功就告诉mq发送确认消息,如果失败就告诉mq回滚消息
3)如果发送了确认消息,此时B系统就会接收到确认消息,然后执行本地事务
4)mq会定时轮询所有的prepared消息回调接口,判断这个消息是不是本地事务处理失败了,没发送确认的消息是否继续重试还是回滚(一般这里可以查一下本地事务是否执行,如果回滚了,这里也会回滚);避免本地事务回滚,确认消息发送出去
5)如果收到确认消息的系统B业务执行失败了,会不断的重试,直到成功;或者B业务回滚,通知A系统,对应的业务也一起回滚;或者发送报警,人工来处理回滚或者补偿,这个场景目前使用会多一点
上游服务的confirm,可能看起来是成功,但是没发送成功,本地已经修改为已发送。
channel.confirmSelect();
// 发送消息
if(channel.waitForConfirms()){
// 发送成功
} else {
// 重试
}
最大努力通知
1)系统A本地事务执行完之后,发送消息到MQ
2)最大努力通知服务,会从MQ消费然后写入数据库中记录下来,或者放内存队列,接着调用B系统
3)如果B执行成功就没问题了;如果B执行失败,最大努力通知系统就会定时尝试重试调用系统B,反复N次,最好还是不行就放弃
CAP
就是Consistency、Avaliability、Partition Tolerence的简称,简单来说就是一致性、可用性、分区容忍性;CA或CP
BASE
Basicly Avaliable、Soft State、Eventual Consistency,基本可用、软状态、最终一致性。
分布式框架调研比较
1、tcc-transaction框架
https://github.com/changmingxie/tcc-transaction
看开源项目是否热门就看star,相当于粉丝,点赞,关注。一般热门项目star比较多
国内开源项目算是最热的了,但是对spring cloud支持不好,暂时不使用
2、himly框架
https://github.com/yu199195/hmily
也不错,对各种服务框架支持比较好
但是项目中大量使用xml进行配置,现在基于注解方式配置更普遍,暂时不使用
3、ByteTCC框架
https://github.com/liuyangming/ByteTCC
热度跟himly差不多,而且对注解支持比较彻底一些,而且对spring cloud支持也不错,可以进行tcc事务开发和控制,还支持saga事务,长事务。
ByteTCC
在bytetcc框架中的逻辑,try-confirm-cancel,try和cancel配对,try阶段成功以后如果要回滚,可以通过cancel进行回滚;
但是confirm失败了,默认策略就是让别人confirm成功,失败的confirm会被bytetcc框架不停的重试和调用。
@Compensable中定义confirm、cancel的接口,启动时扫描@Compensable注解的类和针对LocalXADataSource创建一个动态代理;
会启动一些后台定时任务,task,work(启动一个事务恢复的工作任务,系统重启时,从数据库、日志文件加载出没执行完的事务,然后继续执行这个事务);
bytetcc框架针对spring cloud的支持,扩展和拦截spring cloud原生的feign的行为,所有针对@FeignClient的接口调用,都会走到bytetcc里面;
重构了一下ribbon的负载均衡算法,分布式事务的请求过程中要保证,开启分布式的服务,try请求的服务和后边confirm和cancel的服务是一台服务器;
bytetcc框架通过请求头header和响应header来互相传递和同步分布式事务的上下文和信息的;按照调用顺序,bytetcc框架会将分布式事务中调用其他的服务作为一个resource放到一个resourceList中,这样分布式事务中调用了哪些服务,具体状态是成功还是失败,都可以感知到,框架会针对服务,拼接出来一个cancel调用接口的url,使用ResTemplate来发送这个请求,去调用bytetcc内置的一个controller,带过去xid,然后服务可以根据分布式事务id,来执行对应的cancel操作;
如果try都没成功服务就挂掉了,此时可以让已经try成功的服务直接全部cancel掉,如果try都成功了,confirm没有成功,重启后,知道哪个服务没成功,就去调用资金服务的confirm接口,读取和解析没完成的分布式事务的数据和信息(根据日志等信息)
添加@Transaction注解,就可以看到TransactionInterceptor的东西会运行,会拦截掉请求:
1)begin,启动一个事务=>TransactionManager
2)执行目标方法内部的业务逻辑
3)根据目标方法执行的结果,是否成功,或者报错,选择commit/rollback=>TransactionManager
1.2.4. Mq相关
消息队列的好处
解耦、异步、削峰
mq使用选型
Activemq:吞吐量万级;社区活跃度低(几个月一个版本);消息可能会丢失(毫秒级)
Rabbitmq:吞力量万级;社区活跃度高(一个月几个版本);开源社区维护,不会黄;延时低,elang语言开发(不方便阅读源码);管理界面方便完善(微秒级)
Rocketmq:吞吐量十万级;社区活跃度还不错;公司开源(公司战略等,可能会导致项目黄了);java语言开发,阿里内部大量使用(方便阅读源码)(毫秒级);通过设置可以保证消息0丢失
Kafka:吞吐量十万级;开源社区维护;功能简单;通过设置可以保证消息0丢失
如何选型
一般小型公司推荐Rabbitmq,因为功能完善,社区活跃;
中大型有架构部门,技术实力强的推荐使用Rocketmq,java开发,可以自己调整组件等等,不需要担心项目不维护的问题,性能吞吐量等比较高;
大数据、实时计算、日志采集等领域,推荐Kafka,因为是这些领域的模范,比较成熟。
怎么保证幂等性(重复的消息,保证入库的数据只有一份)
插入前先判断数据中是否有数据(通过redis、内存set、数据库唯一键等限制)
rabbitmq镜像集群模式
rabbitmq消息丢失相关以及解决方案
rabbitmq消息顺序错乱问题&rabbitmq如何保证消息的顺序性
kafka高可用架构
kafka消息顺序错乱问题&kafka如何保证消息的顺序性
通过以下几个方面来考虑设计mq
1、mq的可伸缩性(扩展性)
2、考虑要不要落到磁盘(持久性)
3、考虑一下mq的可用性
4、能不能支撑数据的0丢失
1.2.5. es
elasticseaech设计的理念就是分布式搜索引擎,底层还是基于lucene的;核心思想就是再多台机器上启动多个es进程实例,组成es集群。
es中存储数据的基本单位是索引,一个索引差不多类似于mysql中的一张表。index->type->mapping->document->field
index
类似mysql中一张表
type
没法跟mysql对比,一个index里可以有多个type,每个type都差不多,有略微差别
例如 订单数据中,有些食物的,有些是生活使用的订单,大体上差不多,小部分有点差别,最好每个type要一样否则底层存储等,会有浪费
mapping
如果认为type是一个具体类别的表,index代表多个type同属于一个类型,mapping就是type的结构定义,相当于mysql中的表结构
document
相当于mysql中的一行数据
field
相当于一行数据中的一个字段的值
es高可用
接着搞一个索引,可以拆分为多个shard,每个shard存储部分数据。
这个shard有多个备份,就是每个shard都有一个primary shard,负责写入数据,还有几个replica shard,在primary shard写入数据后将数据同步到其他几个replica shard上去。
这个方案,每个shard有多个备份,如果某个宕机了,还有别的在其他机器上,高可用。
es集群多个节点,会自动选举一个作为master节点,这个master节点其实就是干一些管理的工作,比如维护索引元数据,负责切换primary shard和replica shard身份等等
如果master宕机,就会选举一个节点作为master节点
如果非master节点宕机,会由master节点,让宕机节点的primary shard的身份转移到其他机器上repilca shard。如果宕机修复重启后,master节点会控制将缺少的数据同步到新启动的节点,让集群恢复正常。
es写数据过程
查询GET一条数据:
写入的时候,会有一个全局唯一id,doc id(可以自定义,例如订单id,userId等),会根据doc id 进行hash路由到对应的primary shard上面去写入;
查询的时候使用doc id进行hash,判断在哪个shard上,然后随机选择一个节点(协调节点),通过负载均衡(轮询算法)选择一个shard(primary或者replica)获取数据返回到协调节点,再返回给客户端。
1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点)
2)coordinating node,对document进行路由,将请求转发给对应的node(有primary shard)
3)实际的node上的primary shard处理请求,然后将数据同步到replica shard
4)coordinating node,如果发现primary shard后replica shard都搞定后,就返回响应
es读数据过程
es最强大的是全文检索
1)客户端发送请求到任意一个node,成为coordinate node
2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡
3)接收请求的node返回document给coordinate node
4)coordinate node返回document给客户端
es搜索数据过程
1)客户端发送请求到一个coordinate node
2)协调节点将搜索请求转发到所有的shard对应的primary shard或replica shard
3)query phase:每个shard将自己的搜索结果(其实就是一些doc id),返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果
4)fetch phase:接着由协调节点,根据doc id去各个节点拉取实际的document数据,最终返回给客户端
搜索的底层原理,倒排索引,画图说明传统数据库和倒排索引的区别
倒排索引从词的关键字来找文档,传统的索引,通过数据来找关键词
关键词1 -> doc1,doc2
关键词2 -> doc1,doc3
关键词3 -> doc4
写数据底层原理
1)先写入buffer,在buffer里的时候数据是搜索不到的;同时将数据写入translog日志文件
2)如果buffer快满了,或者到一定时间,就会将buffer数据refresh到一个新的segment file中,此时数据不是直接进入segment file的磁盘文件的,而是先进入os cache的,这个过程就是refresh,如果buffer中没有数据,不会refresh,但是会创建一个空的segment file,如果有数据,默认1s执行一次refresh,刷新到新的segment file
默认每隔1秒refresh一次,所以es是准实时的,因为写入的数据1秒后才会被看到。可以手动refresh,就是手动将buffer中的数据刷入os cache中,让数据可以被搜索到
只要数据输入os cache中,buffer就会被清空,不需要保留buffer了,数据在translog里面已经持久化到磁盘去一份数据
3)只要数据进入os cache,此时就可以让这个segment file的数据对外提供搜索了
4)重复1~3步骤,新的数据不断进入buffer和translog,不断的将buffer数据写入一个又一个新的segment file中去,每次refresh完,buffer清空,translog保留,随着这个过程推进,translog会变得越来越大。当translog达到一定长度的时候,会触发commit操作。
5)commit操作发生第一步,就是将buffer中现有的数据refresh到os cache中去,清空buffer
6)将一个commit point写入磁盘文件,里面标识着这个commit point对应的所有segment file
7)强行将os cache中目前所有的数据都fsync 到磁盘文件中去
translog日志文件作用是什么?
在执行commit操作之前,数据要么停留在buffer,要么停留在os cache,都是内存,一旦机器宕机,内存数据全丢失。
所以需要将数据对应的操作写入专门的日志文件(translog日志文件),当机器宕机重启后,es会自动读取translog文件中的数据,恢复到buffer和os cache中。
8)将现有的translog清空,然后再次重启启动一个translog,此时commit操作完成。默认每隔30分钟自动执行一次commit,但是如果translog过大,也会触发commit。整个commit的过程叫做flush操作。可以手动执行flush操作,就是将所有os cache数据刷到磁盘文件中去
9)translog其实也是先写入os cache的,默认每隔5秒刷一次到磁盘中,所以默认情况下,可能有5秒的数据会仅仅停留在buffer或者translog文件的os cache中,如果此时机器挂了,会丢失5秒钟的数据。但是这样性能比较好,最多丢失5秒数据。也可以将translog设置成每次操作必须直接fsync到磁盘,但是性能会差很多
10)如果是删除操作,commit的时候会生成一个.del文件,里面将某个doc标识为deleted状态,那么搜索的时候根据.del文件就知道这个doc被删除了
11)如果是更新操作,就是将原来的doc标识为deleted状态,然后新写入一条数据
12)buffer每次refresh一次,就会产生一个segment file,所以默认情况下是1秒一个segment file,segment file会越来越多,此时会定期执行merge
13)每次merge的时候,会将多个segment file合并成一个,同时这里将标识为deleted的doc给物理删除掉,然后将新的segment file写入磁盘,这里会写一个commit point,标识所有新的segment file,然后打开segment file供搜索使用,同时删除旧的segment file
es里写流程,有4个核心概念,refresh、flush、translog、merge
当segment file多到一定程度,es就会自动触发merge操作,将多个segment file给merge成一个segment file,并删除原来的文件,在merge的时候,发现某条数据被删除,新的segment file文件中是没有的。
数据写入segment file时同时建立好倒排索引
不要期待随手调一个参数就可以应对万能场景
1)性能优化的杀手锏----filesystem cache(os cache,操作系统的缓存)
es的搜索引擎严重依赖于底层的filesystem cache,如果给filesystem cache更多的内存,尽量让内存可以容纳所有的index segment file索引文件,搜索的时候基本都是走内存,会非常快,性能高
尽量只写入要检索的字段(类似于mysql垂直拆分),节省空间,es+habse(适合海量数据简单搜索)
2)数据预热
可以把一些热门数据等,通过一个系统,先刷新到缓存中,这样访问的时候就会快了
3)冷热分离
es可以类似于水平拆分,访问少的频率低的数据,单独写一个索引,频繁的数据写另一个索引,可以确保热数据预热后,尽量都留在filesystem cache中
4)document模型
尽量不要在搜索时不要做连接等计算操作,写数据时,要都计算好,到时候直接查询检索就好
5)分页性能优化
es分页比较坑,假如每页10条,现在要查询第100页,实际上会把每个shard上的前1000条都查询到一个协调节点上,如果有5个shard就5000条数据,一起合并处理等等,最终获取100页的10条数据,翻页越深,性能越差
<1>不允许深度分页/深度分页性能很差
<2>流式分页,一页一页向下翻页,不能跳转指定页
可以考虑用scroll来进行处理,原理实际上是保留一个数据快照,然后一定时间内不断的滑动往后翻页的时候,类似现在的浏览微博,不断的往后刷新翻页。用scroll不断的通过游标获取下一页,性能还是很高的,比实际翻页好很多
这种不能直接跳转到指定的页,同时要保留一段时间的数据快照,确保用户不会持续不断的翻页几个小时。
中小型公司版本
1)es生产集群部署5台机器,每台机器是6核64G,集群总内存是320G
2)es集群数据量日增大概2000万条,每日增量数据大小大概500M,每月增量6亿条,15G,目前已经·运行几个月了,es集群里的数据大概100G
3)目前有5个索引(结合业务,看看有哪些可以放入es),每个索引数据量大概是20G,所以这个数据量之内,每个索引分配的是8个shard,比默认多3个shard