目录
- 1. 分布式系统下见到的专业术语
- 2. CAP是什么?
- 3. 接口的幂等性如何设计
- 4. 分布式事务(老难了,但非常重要)
- 5. 如何实现分布式锁(重要)
- 6. 限流算法
- 7. 数据库如何处理海量数据
- 8. 分布式ID
- 9. 怎么处理微服务之间的链路追踪?traceId和大数据日志采集的实现原理
- 10. 高并发秒杀场景下的库存超卖解决方案
- 10. 分布式锁对扣减库存的性能影响,并发量大用乐观锁将压力转移到mysql是否合理
- 11. 分布式锁,查库存和扣减库存原子性
- 如何将长链接转化为短链接,并发送短信(了解)
1. 分布式系统下见到的专业术语
1. QPS(Queries Per Second)
每秒查询率 ,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准, 即每秒的响应请求数,也即是最大吞吐能力。
2. TPS(Transactions事务 Per Second)
事务数/秒。一个事务是指一个客户机向服务器发送请求(含事务的操作)然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,
3. 并发数
指系统同时能处理的请求数量,同样反应了系统的负载能力。这个数值可以分析机器1s内的访问日志数量来得到
白话文:常常说的并发高,并发高,说的就是接口的请求数量多(准确来说是系统,不止单个接口哈)
4. 吐吞量
指系统在单位时间内处理请求的数量,TPS、QPS都是吞吐量的常用量化指标。
1. 系统吞吐量影响因素
- 一个系统的吞吐量(承压能力)与request(请求)对cpu的消耗、外部接口、IO等等紧密关联。
- 单个request 对cpu消耗越高、外部系统接口、IO影响速度越慢,系统吞吐能力越低,反之越高。
白话文:一个请求对cpu的消耗、三方接口、IO响应速度都会影响吞吐量
2. CAP是什么?
1. 一致性(Consistency)
这个和数据库ACID的一致性类似,但这里关注的所有数据节点上的数据一致性和正确性,而数据库的ACID关注的是在在一个事务内,对数据的一些约束。系统在执行过某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读取到最新值。
个人理解:关注点在业务逻辑上的一致性
1. 一致性的分类
1. 强一致性:
当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。
白话文:写一个值,就立马能读到什么
2. 弱一致性:
系统并不保证续进程或者线程的访问都会返回最新的更新过的值。用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
白话文:写一个值,但我不确定能不能读取到,也不确定什么时候能读取到(好渣呀)
3. 最终一致性:
是弱一致性的一种特例。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。
白话文:写一个值,如果过一段时间(这个时间依据什么?TODO)没后续的更新了,那么下次就可以读取到这个值
2. 可用性(Availability)
每一个操作总是能够在一定时间内返回结果。需要注意“一定时间”和“返回结果”。“一定时间”是指,系统结果必须在给定时间内返回。“返回结果”是指系统返回操作成功或失败的结果。
接口反馈速度越快,可用性越高;还要保证一定能返回数据
3. 分区容忍性(Partition Tolerance)
是否可以对数据进行分区。这是考虑到性能和可伸缩性。
这个一般情况下都是满足的,所以基本都是CP、AP
3. 接口的幂等性如何设计
1. 幂等性的定义
就是说一个接口,多次发起同一个请求,如何这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。
白话文:同一接口多次请求,保证结果是对的(一般是更新操作,查询当然是一样的)
2. 例子
1. 一个订单扣款两次
假如你有个服务提供一些接口供外部调用,这个服务部署在了 5 台机器上,接着有个接口就是付款接口。然后人家用户在前端上操作的时候,不知道为啥,总之就是一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,好了,结果一个订单扣款扣两次。
2. 网络超时,订单重试调用支付系统
订单系统调用支付系统进行支付,结果不小心因为网络超时了,然后订单系统走了前面我们看到的那个重试机制,咔嚓给你重试了一把,好,支付系统收到一个支付请求两次,而且因为负载均衡算法落在了不同的机器上
3. 解决方案(思路)
其实保证幂等性主要是三点:
1. 请求唯一标识
每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。
2. 消费请求后,要有记录
每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
3. 消费请求前,要判断是否处理过
每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
白话文:1.每个请求要有唯一标识 2.数据库要有记录 3.有更新操作前先去判断一个这个请求是否处理过
4. 实现
1. 用日志表实现
利用一张日志表来记录已经处理成功的请求的ID,如果新到的请求ID已经在日志表中,那么就不再处理这个请求
2. 用redis实现(用set类型)
value存放请求的唯一标识,当存进去的时候才能处理请求,可以用set类型实现(判断元素是否在set集合中:sismember key member)
4. 分布式事务(老难了,但非常重要)
1. 什么是分布式事务(事务涉及多个服务)
指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
白话文:一次事务操作涉及多个服务,保证这些服务的操作一致性。
1. 题外话:多个服务共用一个数据库,也会出现分布式事务问题吗?
会的,我个人认为,上游采用本地事务,调用完接口后,提交了本地事务,下游成功与否感知不到。。。TODO
2. 场景案例
做系统拆分(单机拆成微服务)的时候几乎都会遇到分布式事务的问题,一个仿真的案例如下:
关键字:订单模块、钱包模块、系统拆分
1. 单机应用改为微服务
项目初期,由于用户体量不大,订单模块和钱包模块共库共应用(大war包时代),模块调用可以简化为本地事务操作,这样做只要不是程序本身的BUG,基本可以避免数据不一致。后面因为用户体量越发增大,基于容错、性能、功能共享等考虑,把原来的应用拆分为订单微服务和钱包微服务,两个服务之间通过非本地调用操作(这里可以是HTTP或者消息队列等)进行数据同步,这个时候就很有可能由于异常场景出现数据不一致的情况。
2. 业务场景
订单服务调用钱包服务进行扣款操作,之后修改订单状态为已支付
1. 简单实现以及隐藏问题
以上面的订单微服务请求钱包微服务进行扣款并更新订单状态为扣款这个调用过程为例,假设采用HTTP同步调用,项目如果由经验不足的开发者开发这个逻辑,可能会出现下面的伪代码:
1. 思路
- HTTP调用成功,事务正常提交,业务正常完成
- HTTP调用失败抛出异常会导致事务回滚,用户重试即可
2. 步骤(把Http调用直接放到事务里完成强一致性)
[开启事务]
1、查询订单
2、HTTP调用钱包微服务扣款
3、更新订单状态为扣款成功
[提交事务]
白话文:对对对,年轻的程序员都用这种方法!强一致的前提是没有出现网络波动!
3. 隐藏问题
问题的根本原因是HTTP调用存在网络等问题
1. 长事务,占用数据库连接(耗资源)
由于钱包微服务本身各种原因导致扣款接口响应极慢,会导致上面的处理方法事务(准确来说是数据库连接)长时间挂起,持有的数据库连接无法释放,会导致数据库连接池的连接耗尽,很容易导致订单微服务的其他依赖数据库的接口无法响应
白话文:HTTP调用时间太久,延长了事务的时间,占用数据库连接太久,其他接口有意见
2. 下游服务挂了,上游直接不可用(可用性降低)
钱包微服务是单节点部署(并不是所有的公司微服务都做得很完善),升级期间应用停机,上面方法中第2步接口调用直接失败,这样会导致短时间内所有的事务都回滚,相当于订单微服务的扣款入口是不可用的。
白话文:下游在部署中,导致订单事务频繁失败回滚,功能直接不能用了
3. 下游成功,但因为网络原因,上游没收到,上游回滚(数据不一致问题)
网络是不可靠的,HTTP调用或者接受响应的时候如果出现网络闪断有可能出现了服务间状态不能互相明确的情况,例如订单微服务调用钱包微服务成功,接受响应的时候出现网络问题,会出现扣款成功但是订单状态没有更新的可能(订单微服务事务回滚)。
白话文:钱包服务扣款成功,因为网络问题,没能告诉订单服务,订单服务回滚(白付钱了)
这种解决方案,只能每天祈求下游服务或者网络不出现任何问题
2. 事务中进行异步消息推送
使用消息队列进行服务之间的调用也是常见的方式之一,但是使用消息队列交互本质是异步的,无法感知下游消息消费方是否正常处理消息。假设采用消息队列异步调用,项目如果由经验不足的开发者开发这个逻辑,可能会出现下面的伪代码
1. 思路
同步响应太慢,那我用消息队列异步去执行任务,总快吧,但是下游服务能不能处理好消息我就不管了
2. 步骤
[开启事务]
1、查询订单
2、推送钱包微服务扣款消息(消息队列实现推送消息)
3、更新订单状态为扣款成功
[提交事务]
3. 隐藏问题(中介挂了、上下游无法互相感知)
这样做,在正常情况下,也就是能够正常调用消息队列中间件且推送消息成功的情况下,事务是能够正确提交的。但是存在两个明显的问题:
1. 消息队列挂了,上游接口不可用(可用性降低)
消息队列中间件出现了异常,无法正常调用,常见的情况是网络原因或者消息队列中间件不可用,会导致异常从而使得事务回滚。这种情况看起来似乎合情合理,但是仔细想:为什么消息队列中间件调用异常会导致业务事务回滚,如果中间件不恢复,这个接口调用岂不是相当于不可用?
=>白话文:消息队列炸了,消息都推不过去,导致一直回滚,导致接口不可用
2. 下游异常出现异常,上游无法感知到,上游进行回滚(数据不一致问题)
就祈祷下游不会出现问题吧
3. 下游执行成功后,上游又出现问题了,下游无法回滚(数据不一致问题)
如果消息队列中间件正常,消息推送正常,但是之后由于下游SQL存在语法错误导致事务回滚,这样就会出现了下游微服务被调用成功,本地事务却回滚的问题,导致了上下游系统数据不一致。(这个问题我在做项目的时候遇到过!是采用完成本地事务之后再推送数据解决的,调整了一下代码顺序,其实还是有问题的,要保证下游不会出现问题才行)
白话文:异步推送成功,但推送之后代码出问题导致本地事务回滚,没办法通知下游服务一起回滚
4. 总结
事务中进行异步消息推送是一种并不可靠的实现
当然!RocketMQ除外!别急别急,下面会细说的
3. 分布式事务的解决方案(难、重要)
来看看业内的大佬们是如何解决的
原文链接:多阶段提交方案(2PC、3PC)
原文链接:面试必问:分布式事务六种解决方案
业界目前主流的分布式事务解决方案主要有:多阶段提交方案(2PC、3PC)、补偿事务和消息事务(主要是RocketMQ,基本思想也是多阶段提交方案,其他消息队列中间件并没有实现分布式事务
1. 多阶段提交方案(2PC、3PC)
常见的有二阶段和三阶段提交事务,需要额外的资源管理器来协调事务,数据一致性强,但是实现方案比较复杂,对性能的牺牲比较大(主要是需要对资源锁定,等待所有事务提交才能解锁),不适用于高并发的场景,目前比较知名的有阿里开源的fescar。
白话文:对操作的所有资源锁定,强一致性,并发就低了
1. XA协议是什么(分布式事务协议)
关键字:协调者、事务参与者、询问
2. 组成
1. 事务管理器(协调者)
事务管理器作为全局的调度者,负责各个本地资源的提交和回滚
2. 本地资源管理器(事务参与者)
本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口
3. XA协议的相关内容(记录写前日志、持久化存储)
-
协议中假设每个节点都会记录写前日志(write-ahead log)并持久性存储(如mysql中的redolog和undolog),即使节点发生故障日志也不会丢失。
-
协议中同时假设节点不会发生永久性故障而且任意两个节点都可以互相通信。
白话文: 2个假设,1.写之前记录日志并持久化(用于回滚);2.服务器不会炸且没有网络问题
4. 原理(流程图)
XA实现分布式事务的原理如下:
图片来源:https://www.cnblogs.com/cxxjohnson/p/9145548.html
白话文:上图可知事务管理器又叫协调者,本地资源管理器又叫事务参与者
2. 二阶段提交(2PC)
两阶段提交协议是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。
吐槽:为啥叫协调这么难记,事务参与者会打架吗?
1. 协议参与者(1个协调者 + 多个事务参与者)
在两阶段提交协议中,系统一般包含两类机器(或节点)
1. 协调者
通常一个系统中只有一个;
2. 事务参与者
一般包含多个,在数据存储系统中可以理解为数据副本的个数。
白话文:1个协调者 + 多个事务参与者
2. 执行流程(哪两个阶段)
1. 请求阶段(协调者咨询,事务参与者表决)
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。
白话文:老板(协调者)组织团建,让员工表决(事务参与者),员工有(同意)也有不同意的(取消)
2. 执行阶段(协调者决策选择提交或回滚,事务参与者执行)
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或回滚。
当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。
参与者在接收到协调者发来的消息后将执行响应的操作。
白话文:老板(协调者)看到所有员工都(同意),才决定去团建(提交事务),只要有一个不同意,就取消团建(回滚取消事务)
3. 存在的问题
1. 同步阻塞问题(资源锁定)
执行过程中,所有参与节点都是事务阻塞型的。
当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
事务参与者用到的资源被锁定
2. 协调者只有一台(单点故障问题,资源一直阻塞)
由于协调者的重要性,一旦协调者发生故障。
参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
协调者是单节点的
3. 执行阶段,网络原因或部分事务参与者挂了,感知不到(数据不一致问题)
在执行阶段,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中事务参与者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是部分挂掉的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
白话文:老板发通知的时候,刚好有部分员工睡着了没听到,然后一直在等待结果(无法执行事务提交)
4. 解决方案(超时机制、互询机制)
1. 超时机制
- 对于协调者来说,指定时间内没有收到所有事务参与者的ACK应答,则发生回滚通知
- 对于事务参与者来说,未收到协调者的通知,需要有互询机制做判断
2. 互询机制
- 对于事务参与者来说,询问其他事务参与者的状态:
- 如果如果 B 执行了 rollback 或 commit 操作,则 A 可以大胆的与 B 执行相同的操作
- 如果 B 此时还没有到达 READY 状态,则可以推断出协调者发出的肯定是 rollback 通知;
- 如果 B 同样位于 READY 状态,则 A 可以继续询问另外的参与者。只有当所有的参与者都位于 READY 状态时,此时两阶段提交协议无法处理,将陷入长时间的阻塞状态。(这个情况没办法了)
3. 三阶段提交(3PC)
关键字:超时机制、询问阶段、响应反馈
三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。
1. 询问阶段(协调者询问,事务参与者返回Yes or No响应,轻量级的操作)
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
白话文:询问是否可以正常执行
2. 预提交阶段(PreCommit)
协调者根据事务参与者的第一步反应情况来决定是否可以继续事务的PreCommit操作。
根据响应情况,有以下两种可能。
1. 假如返回的都是Yes响应(执行预提交)
那么就会进行事务的预执行:
- 发送预提交请求。协调者向事务参与者发送预提交请求,并进入Prepared阶段。
- 事务预提交。事务参与者接收到预提交请求后,会执行事务操作,并将undo和redo信息(mysql的undolog和redolog?)记录到事务日志中。
- 响应反馈。如果事务参与者成功的执行了事务操作,给协调者发送ACK响应,同时开始等待最终指令
2. 假如返回中有一个No响应或未响应(执行中断事务)
假如有任何一个No响应,或者等待超时之后,就中断事务:
- 协调者发送中断请求。协调者向所有事务参与者发送abort请求。
- 事务参与者中断事务。事务参与者收到abort请求之后(或超时之后,仍未收到事务参与者的请求),执行事务的中断。
白话文:询问好了,都OK的话,就发送预提交给事务参与者,事务参与者进行事务预提交,记录2个日志;否则,中断事务就可以(这时候不用回归)
3. 确认提交阶段(doCommit)
该阶段进行真正的事务提交,也可以分为以下两种情况:
1. 执行提交
- 发送提交请求:协调者接收到所有事务参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有事务参与者发送确认提交请求。
- 事务提交:事务参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
- 响应反馈:事务提交完之后,向协调者给事务参与者发送ACK响应。
- 完成事务。协调者接收到所有事务参与者的ACK响应之后,完成事务。
老板(协调者)通知了员工(事务参与者)确定去团建,所以员工(事务参与者)都知道了并给老板(协调者)响应(ACK响应),团建的事就敲定了(完成事务)
2. 中断事务
协调者没有接收到事务参与者发送的ACK响应(可能是发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
老板(协调者)通知了员工(事务参与者)确定去团建,但还是有员工(事务参与者)没给老板(协调者)响应(ACK响应),就会触发中断事务
4. 问:预提交和确认提交有什么区别?
- 预提交的结果是产生undo和redo日志,并没有commit操作
- 确认提交是真正的事务提交,确认提交后会释放资源
4. 多阶段提交的优点
1. 数据一致性强
通过redo、undo日志,执行提交和回滚操作
5. 多阶段提交的缺点
1. 需要锁定资源
对性能的牺牲比较大,对资源锁定,等待所有事务提交才能解锁,不适用于高并发的场景,目前比较知名的有阿里开源的Seata
2. 需要额外的资源(协调者)
需要额外的资源管理器来协调事务
3. 实现复杂
想想都感觉好复杂。。。
2. 补偿事务(TCC:Try + Confirm + Cancel)
一般也叫TCC,因为每个事务操作都需要提供三个操作尝试(Try)、确认(Confirm)和补偿/撤销(Cancel),数据一致性的强度比多阶段提交方案低,但是实现的复杂度会有所降低(低??确实),比较明显的缺陷是每个业务事务需要实现三组操作,有可能出现过多的补偿方案的代码;另外有很多场景TCC是不合适的。
白话文:TCC,有三个操作(尝试、确认、撤销),每个事务的业务都要写这三个步骤,代码开发量变多了,
tips个人理解:Try生成一个草稿(预状态),comfirm把草稿变成真的,cancel删除草稿
图片来源:https://www.cnblogs.com/jajian/p/10014145.html
1. 操作步骤
1. Try阶段
主要是对业务系统做检测及资源预留,一般都是锁定某个资源,设置一个预备状态,冻结部分数据
白话文:每个服务先锁住要用的资源,之后一起执行
1. 预备状态指什么?
其实就是一个没有实际业务含义的状态,比如说修改中、数据冻结中,又或者是一个预增加的字段
2. Confirm阶段
主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
白话文:执行业务,释放对资源的锁,返回成功
3. Cancel阶段
主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
白话文:执行失败了,回滚代码,释放对资源的锁,返回失败
2. TCC分布式事务框架有哪些?
国内开源的 ByteTCC、Himly、TCC-transaction等
3. 实现
tcc分布式事务在这里起到了一个事务协调者的角色。真实业务只需要调用try阶段的方法。confirm和cancel阶段的方法由tcc框架来帮我们调用完成最终业务逻辑
3. 本地消息表(实现的是最终一致性)
图片来源:https://www.jianshu.com/p/e31d9ebed201
本地消息表其实就是利用了各系统本地的事务来实现分布式事务。
1. 本地消息表的组成
1. 消息状态字段
预存状态、成功状态、失败状态
2. 业务相关字段
比如说请求的唯一识别
3. 重试次数字段
2. 如何实现(将业务逻辑和本地消息表的操作放在一个事务中)
白话文:1.本地创建一张消息表,将业务的操作和将消息放入消息表的操作放到同一个事务中。2.有一个定时任务,处理本地消息表的未成功的数据,不断重试重试,如果还不行就人工处理这些数据
1. 消息表插入一条预存状态消息
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
2. 业务成功,修改消息状态为成功
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
3. 定时任务扫描失败的消息,重新执行
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
4. 超过最大重试次数,人工处理
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
3. 目的(最终一致性)
本地消息表实现的是最终一致性,容忍了数据暂时不一致的情况
4. 消息事务(RocketMQ实现、半消息)
图片来源:知乎敖丙
1. RocketMQ实现流程
1. 上游给Broker发送半消息
先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见
2. 执行上游本地事务
发送成功后发送方再执行本地事务
3. 发送 Commit 或 RollBack 命令
根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令
并且 RocketMQ 的发送方会提供一个反查事务状态接口(check接口),如果一段时间内半消息没有收到任何操作请求(超时检测),那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
4. 下游消费失败会不断重试,知道成功为止
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
白话文:上游给MQ发送一个半消息,并提供一个check接口,然后去执行本地事务,check接口检测本地事务执行完成后,MQ把半消息变成真消息,下游可见可消费,下游消费失败会不断重试
5. 最大努力通知(是一种思想:尽可能的完成最终一致性)
本地消息表也可以算最大努力,消息事务也可以算最大努力。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。
6. Saga相关(正向重试、逆向回滚)
1. Saga模型是什么
它是由多个有序的事务组成、并且与其他事务可以交错的一个长时间事务(LLT:long lived transaction?)
2. 现状
一个长时间事务会在相对较长的时间内占用数据库资源,明显的阻碍了较短的和公用的其他事务完成。
白话文:长时间事务引发的问题
3. Saga解决了什么
saga讲述的是如何处理long lived transaction(长活事务),一个系统中如果流程很长,如果需要保证数据库ACID特性,则事务时间很长,这样容易出现死锁错误。
4. Saga如何解决LLT的
对系统进行分布式拆分,形成多个子系统(子事务);由一个协调者统一管理,最终统一的提交/回滚,则可以避免事务时间过长的问题(需要注意的是,这里的提交和回滚是业务逻辑层面的,而非数据库)。
白话文:把长活事务拆分子事务,提供了正向重试或者逆向回滚
5. Saga和XA以及TCC对比
- Saga区别于XA(二阶段提交),是基于对服务的统一调度,而非资源管理器(这一点和TCC模型一样)。(看不懂,过)
- Saga区别于TCC(补偿事务),不提供预留(try)步骤;只提供2个步骤:
- 业务代码执行并提交
- 业务代码执行逻辑回滚
- TCC是三个接口选择其中两个(try-confirm或try-cancel);saga是如果成功则没有其他动作了,如果失败则正向重试或者逆向回滚
原文地址:https://copyfuture.com/blogs-details/20210129153450331v?ivk_sa=1024320u
6. Saga例子(加深印象)
假设我们要实现一个转账场景,我们先设想一下Saga模型如何在代码中实现:
首先有三个独立的服务:transfer(转账入口)、bank1(转出银行)、bank2(转入银行)。
其中bank1、bank2都提供一个正向的接口:bank1扣钱,bank2加钱。
然后bank1、bank2都提供一个逆向接口,也就是回退接口:bank1加钱(把失败的钱加回去),bank2扣钱(把失败的转账再扣掉)。
transfer提供服务入口;并且进行try-catch-finally代码块:
transfer先后调用bank1、bank2正向转账接口,如果整体调用成功,则完成。
如果其中有一步失败(假设是bank1调用成功,bank2失败),则有两个选择:
1.重试bank2以及后续各个服务(正向恢复)
2.调用bank1回滚接口 (逆向恢复)
如果有多个子服务,就顺序执行这个逻辑。
如果逆向恢复也出错了咋办?
- 假设最终能成功,多试几次
- 假设有几率失败,则根据相关日志记录进行人工干预
白话文:每个服务提供了正向接口和逆向接口,如果出现调用失败,1:自己想努力一下,不断重试失败的正向接口 2:自己放弃了,让其他服务调用他们自己的逆向接口
逆向回滚要自己用代码去实现,代码复杂的话,疯了。。。
5. 如何实现分布式锁(重要)
灵魂拷问:为什么要用分布式锁?
1. 锁的目的是什么?
为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行
2. 单体应用是如何处理锁的?ReentrantLcok、synchronized等
在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制
3. 分布式系统为什么不能用这写Java并发相关的API?(不同机器上)
分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效
4. 分布式锁有什么作用(跨JVM)
为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
1. 分布式锁实现要满足的要求
主要要求:
- 互斥性:在任意时刻,只有一个客户端的一个线程能持有锁。
- 不能死锁:客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
- 高可用:获取分布式锁的服务不能挂了,至少要集群,主从等等保证高可用
- 高性能:能快速的获取锁和释放锁
次要要求:
- 具备可重入特性
2. 基于数据库实现
1. 基于表数据的插入删除
创建一张表,里面方法名称字段为唯一的。想要执行某方法的时候向该表执行插入操作,执行完成之后删除该记录即可。因为方法名称字段唯一,所以在并发的时候只能插入一条记录,其他的并不会执行。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
白话文:建一张表,方法名称字段是唯一识别,插入数据成功(代表获取锁成功),插入数据失败(代表获取锁失败),删除数据成功(代表释放锁成功)
1. 存在的问题
1. 数据库是单点的,可用性不高
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
2. 会出现死锁,因为锁没有失效时间
一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
3. 获取不到锁,会直接报错而不会等待
这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
4. 不可重入
这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
2. 解决(太麻烦了吧,要集群、定时任务、While、加新字段)
- 数据库搞个集群:数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
- 定时任务定时清理数据:没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
- 获取不到锁就多试几次,并设置最大尝试次数:非阻塞的?搞一个while循环,直到insert成功再返回成功。
- 加字段实现可重入:非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。(但是不能一步一步释放锁,还是有问题呀)
2. 基于数据库排它锁(对查询操作加排他锁)
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。
我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁(for update)来实现分布式锁。 基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:
1. 加锁
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁,当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,直接catch报错,延迟,进入下次循环
白话文:第一步:关闭事务自动提交;第二步,对查询操作加排他锁(注意:这个表是一个空表。里面可以没有任何数据,查出来为null,只要保证查询不报错就行,只用到了数据库排他锁的这个特性)
2. 解锁
白话文:提交事务就可以了
2. 基于Redis实现(重要)
关键字:setex、LUA算法
原文链接:敖丙之如何用Redis实现分布式锁
1. Redis涉及命令
1. setnx(SET if Not eXists)
SETNX key value
- key不存在:set成功,返回int的1
- key存在,set失败,直接返回0
2. setex(这里的e代表expire的意思吗)
SETEX key seconds value
- key不存在:set成功,关联value并设置key的过期时间
- key存在:覆盖旧值
setex的操作是原子性操作,set value和set expire会同时完成
2. 实现
1. 加锁
2. 使用setnx加锁
存在的问题:setnx加锁后没有过期时间,一旦服务加锁后,突然出问题了,没有释放锁,会导致锁一直被占用
3. 使用setnx+set expire加锁
存在的问题:setnx+set expire是是非原子性操作,可能setnx成功了,但在set expire之前出故障了,还是会有问题
4. 使用setex加锁(最佳实现,setnx+setpx的结合)
setex是原子性操作,可以避免这个问题
5. 解锁(删除key,使用Lua算法)
解锁的逻辑更加简单,就是一段Lua的拼装,把Key做了删除。
public boolean unlock(String id) {
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
String result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
return "1".equals(result) ? true : false;
} finally {
jedis.close();
}
}
Lua表达式用代码中的String接一下
3. 注意事项(谁加锁就谁解锁)
解决方案:value可以存这个服务的信息,删之前先校验一下是不是自己存的key
4. 问:锁提前过期后,线程A还没执行完,然后线程B获取到了锁,然后线程A执行完了,把B的锁给删掉了怎么办?
- 使用延迟队列监听过期时间的redis key(续命)
- 在获取锁的时候给value设置一个随机值,删除key的时候先判断这个value值是不是自己线程的,如果不是,则不能删除
白话文:谁加锁必须由它自己解锁,其他线程不能解锁,不然就乱套了
如何使用延迟队列可以参考文章:https://www.jianshu.com/p/6ee386dd166d
3. 基于Redisson实现(在Redis的基础上可重入)
Redisson是封装了Redis实现的工具类
ThreadPoolExecutor threadPoolExecutor =
new ThreadPoolExecutor(inventory, inventory, 10L, SECONDS, linkedBlockingQueue);
long start = System.currentTimeMillis();
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
final RedissonClient client = Redisson.create(config);
// 获取一个锁的实例
final RLock lock = client.getLock("lock1");
for (int i = 0; i <= NUM; i++) {
threadPoolExecutor.execute(new Runnable() {
public void run() {
lock.lock();
inventory--;
System.out.println(inventory);
lock.unlock();
}
});
}
long end = System.currentTimeMillis();
System.out.println("执行线程数:" + NUM + " 总耗时:" + (end - start) + " 库存数为:" + inventory);
1. 加锁(计数器+1)
尝试去获取锁,和Lock类有点相似,加锁的内部使用LUA语法,具体见原文
主要是判断锁是否存在,不存在就设置过期时间,占用锁;如果锁已经存在了,那对比一下线程,线程是一个那就证明可以重入,锁在了,但是不是当前线程,证明别人还没释放,那就把剩余时间返回,加锁失败。
白话文:尝试去获取锁,如果锁存在,且是当前线程,那么给计数+1(hincrby给数值递增1)
2. 解锁(计数器-1)
锁的释放主要是publish释放锁的信息,然后做校验,一样会判断是否当前线程,成功就释放锁,还有个hincrby递减的操作,锁的值大于0说明是可重入锁,那就刷新过期时间。
如果值小于0了,那删掉Key释放锁。
白话文:判断是否当前线程,是的话计数-1(hincrby递减1),如果计数值小于0,则释放锁(和AQS的status有点像)
4. 基于zookpper实现分布式锁(重要,有空再了解)
TODO
原文链接:https://blog.youkuaiyun.com/qq_21873747/article/details/79485814
6. 限流算法
1. 为什么要对接口限流
- 系统处理能力有限,对于那些已经超出系统处理能力的请求我们应该尽可能早的识别出来并让其等待或拒绝这些请求,如果当大流量进入系统的时候不进行限流,那么将超出系统的负载,这种情况会导致服务异常、宕机等情况的出现。
- 防止黑客恶意攻击
2. 计数器算法(又叫固定窗口算法)
比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多。
如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter
白话文:看图片秒懂好吧
个人理解:设定时间T,设定最大容量maxSize,设置计数器counter,来一次请求counter+1,如果在T时间counter数大于maxSize,对接口限流,否则把counter清零,到下个T时间段
1. 计数器算法的问题(临界问题,不是缺陷,是大问题)
白话文:在时间窗口的重置节点处突发请求,但是计数器算法检测不到,导致系统崩了
通过上图我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在1秒里面,瞬间发送了200个请求。
我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的系统。
导致这个问题出现的原因是我们统计的精度太低。
2. 解决方案
滑动窗口算法
3. 滑动窗口算法
在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。
1. 滑动窗口怎么解决之前的临界问题
我们可以看上图,0:59到达的100个请求会落在灰色的格子中,而1:00到达的请求会落在橘黄色的格子中。当时间到达1:00时,我们的窗口会往右移动一格,那么此时时间窗口内的总请求数量一共是200个,超过了限定的100个,所以此时能够检测出来触发了限流。
小tips:计数器算法可以看成只有一格的滑动窗口算法。(然并卵)
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
2. 具体例子(sentinel)
TODO
4. 漏桶算法(流量整形哈哈哈)
漏桶算法(Leaky Bucket)是网络世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。
在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
1. 特性
关键字:流入请求、流出请求
- 漏桶具有固定容量,出水速率是固定常量(流出请求)
- 如果桶是空的,则不需流出水滴
- 可以以任意速率流入水滴到漏桶(流入请求)
- 如果流入水滴超出了桶的容量,则流入的水滴溢出(新请求被拒绝)
流入请求速度多快无所谓,只要不超出桶的容量就行,流出请求的速度恒定不变
2. 缺陷
- 在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使某一个单独的流突发到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。
系统性能突然变得特别好,但是流出请求的速度因为之前的缘故设定死了,改不了,只能慢慢来,系统的资源不能得到充分利用
3. 解决方案
令牌桶算法
4. 令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。
典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
白话文:令牌生产速度恒定,生产后的令牌会丢到桶里去,如果桶塞满了,就停止生产令牌
传送到令牌桶的数据包需要消耗令牌,不同大小的数据包,消耗的令牌数量不一样。
令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。
白话文:令牌就是一个字节,桶就是存放字节的,有最大容量
如果令牌桶中存在令牌,则允许发送流量。而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。
白话文:1.桶中令牌或者令牌很少(一次请求可能需要消耗多个令牌,不够用),会限流;桶中令牌超多,可以以最快速度处理请求(解决了漏桶算法恒定速度的问题)
1. 目的(可以处理短暂的突发流量)
7. 数据库如何处理海量数据
1. 读/写分离
主库负责写,从库负责读(待扩充。。。TODO)
2. 垂直拆分(列的拆分)
垂直拆分是指对数据表列的拆分
1. 定义
把一张列比较多的表拆分为多张表
2. 哪些列需要拆分
1. 更新频繁的字段
如用户最近一次登录时间
2. 大字段
text类型,如个人介绍
3. 优点
1. 简化表结构,查询速率变快
使得列数据变小,在查询时减少锁占用时间,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护
4. 缺点
1. 主键要求保持一致
主键会出现冗余,拆完后的两个表主键必须保持一致,这个需要代码去控制
2. 事务问题
事务处理变得复杂,需要对拆分后的表同时执行事务操作
3. 水平拆分(行的拆分)
水平拆分是指数据表行的拆分
1. 定义
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表(表1、表2、表3.。。。。。)或者库(不同库就可以用同样的表名了)中,达到了分布式的目的
2. 哪些表需要水平拆分
1. 无穷增长的表
对于一些无穷增长的表(如操作日志表),提升机器性能、增加存储的方式解决不了,这时候就要对表进行水平拆分,拆层多个相同的表
注意:水平拆分建议分库水平拆分,放到不同的服务器上,这样才能有效缓解单台服务器的压力
3. 优点
解决了无穷增长的表的问题
4. 缺点
1. 事务问题
分片事务难以解决 ,跨节点Join性能较差,逻辑复杂
5. 水平拆分后的事务问题如何解决(Sharding-JDBC)
1. 客户端代理(Sharding-JDBC)
分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
2. 中间件代理
在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
更多具体的实现方式可以参考文章:
MySQL 对于千万级的大表要怎么优化?
3. 如何利用Sharding-JDBC进行分库分表
1. Sharding-JDBC介绍
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
- 可适用于任何基于java的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
- 可基于任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid等。
- 理论上可支持任意实现JDBC规范的数据库。虽然目前仅支持MySQL,但已有支持Oracle,SQLServer,DB2等数据库的计划。
特点:旧代码迁移成本几乎为零
2. 实现过程
1. 加pom依赖
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-config-spring</artifactId>
<version>1.3.0</version>
</dependency>
2. 修改数据库相关的配置
配置库的IP呀、表名呀等等
3. 编写类SingleKeyTableShardingAlgorithm(分配策略)
这个类用来根据entity_key值确定使用的分表名。参考sharding提供的示例代码进行修改。
SingleKeyTableShardingAlgorithm应该就是分配策略
8. 分布式ID
1. 什么是分布式ID
在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了
个人理解:分布式系统下,在分库分表之后唯一识别ID
2. 分布式ID需要满足的需求
1. 基本需求
- 全局唯一 :ID 的全局唯一性肯定是首先要满足的!
- 高性能 : 分布式 ID 的生成速度要快,对本地资源消耗要小。(生成速度快)
- 高可用 :生成分布式 ID 的服务要保证可用性无限接近于 100%。(服务禁止挂掉!!)
- 方便易用 :拿来即用,使用方便,快速接入!(代码方便写)
2. 其他需求
- 安全 :ID 中不包含敏感信息(如用户)。
- 有序递增 :如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。
- 有具体的业务含义 :生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。
- 独立部署 :也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。(保证高可用)
3. 分布式 ID 常见解决方案
主要是考虑去哪里获取到这个分布式ID,从以下三个维度去分析
1. 数据库维度
1. 基于数据库主键自增实现(一个假表,只有id和无效字段)
1. 如何实现(假insert操作后,取主键ID)
CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
白话文:将假数据插入假表,然后获取这个id
2. 优点
实现起来比较简单、ID 有序递增、存储消耗空间小
3. 缺点
- 每次获取都要访问一次数据库,数据库顶不住
- 支持的并发量不大、存在数据库单点问题(使用主从模式来提高可用性)
2. 基于数据库的号段模式实现(重要,Leaf和Tinyid框架基于这个模式)
1. 如何实现(批量获取,然后保存在内存里)
批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就可以了
号:ID号,段:一段一段的取
CREATE TABLE `sequence_id_generator` (
`id` int(10) NOT NULL,
`current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(10) NOT NULL COMMENT '号段的长度',
`version` int(20) NOT NULL COMMENT '版本号',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- current_max_id 字段和step字段主要用于获取批量 ID,获取的批量 id 为: current_max_id ~ current_max_id+step。
- version 字段主要用于解决并发问题(乐观锁)
- biz_type 主要用于表示业务类型。
相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。另外,为了避免单点问题,你可以从使用主从模式来提高可用性。
2. 优点
- ID 有序递增、存储消耗空间小
- 对于数据库的访问次数更少,数据库压力更小
3. 缺点
存在数据库单点问题(使用主从模式来提高可用性)
2. 基于Redis集群实现
1. 如何实现(incr命令)
使用incr命令保证原子性地递增ID
127.0.0.1:6379> set sequence_id_biz_type 1
OK
127.0.0.1:6379> incr sequence_id_biz_type
(integer) 2
127.0.0.1:6379> get sequence_id_biz_type
"2"
2. 优点
性能不错并且生成的 ID 是有序递增的
3. 缺点
存在单点问题,所以要做成Redis集群
3. 算法维度
1. UUID
比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适:
- 数据库主键要尽量越短越好,而 UUID 的消耗的存储空间比较大(32 个字符串,128 位)。
- UUID 是无顺序的,InnoDB 引擎下,数据库主键的无序性会严重影响数据库性能。
1. 优点
生成速度比较快、简单易用
2. 缺点
- 存储消耗空间大(32 个字符串,128 位)
- 不安全(基于 MAC 地址生成 UUID 的算法会造成 MAC 地址泄露)、无序(非自增)、没有具体业务含义
- 需要解决重复 ID 问题(当机器时间不对的情况下,可能导致会产生重复 ID)
2. 雪花算法(重要)
1 个 bit 是不用的 + 用其中的 41 bit 作为毫秒数 + 用 10 bit 作为工作机器 id + 12 bit 作为序列号
1. 优点
- 生成速度比较快、生成的 ID 有序递增、比较灵活(可以对 Snowflake 算法进行简单的改造比如加入业务 ID)
- 有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化。
2. 缺点
需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。
4. 开源框架维度
1. UidGenerator(百度,基于雪花算法)
UidGenerator 是百度开源的一款**基于 Snowflake(雪花算法)**的唯一 ID 生成器。
2. Leaf(美团,世界上没有两片相同的树叶,基于雪花算法和号段模式)
Leaf 提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。
3. Tinyid(滴滴,基于号段模式)
Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。
9. 怎么处理微服务之间的链路追踪?traceId和大数据日志采集的实现原理
1. 什么是链路追踪?
分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
2. 链路追踪的作用
1. 故障快速定位
可以通过调用链结合业务日志快速定位错误信息
2. 链路性能可视化
各个阶段链路耗时、服务依赖关系可以通过可视化界面展现出来
3. 链路分析
通过分析链路耗时、服务依赖关系可以得到用户的行为路径,汇总分析应用在很多业务场景
4. 原理
通过事先在日志中埋点,找出相同traceId的日志,再加上parentId和spanId就可以将一条完整的请求调用链串联起来
图片链接:https://blog.youkuaiyun.com/dabaoshiwode/article/details/109673681
1. traceId 链路唯一识别
traceId串联请求形成链路,每一条局部链路都用一个全局唯一的traceId来标识
2. parentId 父服务id
表示调用自己服务的父服务,从1开始递增
白话文:说服务id感觉也不太对。。。
3. spanId 使请求具有父子关系
服务A的方法调用了服务B、服务C,但是不知道调用的先后数据,spanId就是表示这个先后顺序的,从1开始,越小的越先调用
5. Zipkin
TODO
10. 高并发秒杀场景下的库存超卖解决方案
1. 库存超卖现象
- 两个线程同时查询库存是否充足,都满足条件,OK
- 两个线程同时对库存进行扣减操作,导致库存为负数的现象,就是库存超卖现象
图片链接:https://www.sohu.com/a/436796808_411876
2. 解决方案
1. 悲观锁(for update)
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作
1. 注意事项
- 关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交
2. 乐观锁
适用原子类比如AtomicLong,内部采用CAS机制实现
1. 问题
- 导致大量线程长时间重复循环,占用CPU
3. 解决方案(LongAdder类、分段CAS操作)
Java 8中新增了一个LongAdder类,也是针对Java 7以前的AtomicLong进行的优化,解决的是CAS类操作在高并发场景下,使用乐观锁思路,会导致大量线程长时间重复循环。
LongAdder中也是采用了类似的分段CAS操作,失败则自动迁移到下一个分段进行CAS的思路
2. 分布式锁
在执行所有方法前,先去获取分布式锁,获取成功后再执行后续操作
图片链接:https://www.sohu.com/a/436796808_411876
2. 优点
保证了数据的准确性
3. 缺点(并发弱、基于分布式锁串行化处理)
- 并发能力有点弱:分布式锁一旦加了之后,对同一个商品的下单请求,会导致所有客户端都必须对同一个商品的库存锁key进行加锁
- 基于分布式锁串行化处理
4. 适用场景
适用低并发、无秒杀的场景
5. 问:如何优化分布式锁的并发性能(分段加锁)
1. 流程
- 库存分段:把1000件库存拆开,每个库存段是50件库存,比如stock_01对应50件库存,stock_02对应50件库存
- 使用随机算法分配请求:每秒1000个请求过来了,此时其实可以是自己写一个简单的随机算法,每个请求都是随机在20个分段库存里,选择一个进行加锁
- 获取到的分段库存为空,需要立马释放锁
图片链接:https://www.sohu.com/a/436796808_411876
参考java里的ConcurrentHashMap的源码和底层原理
2. 缺点
实现太复杂了
- 你得对一个数据分段存储,一个库存字段本来好好的,现在要分为20个分段库存字段
- 你在每次处理库存的时候,还得自己写随机算法,随机挑选一个分段来处理
- 如果某个分段中的数据不足了,你还得自动切换到下一个分段数据去处理
4. 队列串行化
TODO
5. Redis原子操作
TODO
10. 分布式锁对扣减库存的性能影响,并发量大用乐观锁将压力转移到mysql是否合理
TODO
11. 分布式锁,查库存和扣减库存原子性
TODO
如何将长链接转化为短链接,并发送短信(了解)
短URL从生成到使用分为以下几步.
- 有一个服务,将要发送给你的长URL对应到一个短URL上.例如 www.baidu.com->www.t.cn/1
- 把短url拼接到短信等的内容上发送
- 用户点击短URL,浏览器用301/302进行重定向,访问到对应的长URL
- 展示对应的内容
原文地址:https://blog.youkuaiyun.com/yanpenglei/article/details/100788735