分布式

217 阅读46分钟

微信入群一块学习技术:Day9884125

1、分布式锁在项目中有哪些应用场景

使用分布式锁的场景一般需要满足以下场景:
   1、系统是一个分布式系统集群,java的锁已经锁不住了
   2、操作共享资源,比如库里唯一的用户数据
   3、同步访问,即多个线程同时操作共享资源

2、分布式锁有哪些解决方案

   redis的分布式锁,很多大公司会基于redis做扩展开发。用setnx命令,redission框架
   基于Zooleeper,顺序临时节点
   基于数据库,比如mysql,主键或唯一索引的唯一性

3、redis做分布式锁的命令

setnx
   格式:setnx key value将key的值设为value,当且仅当key不存在
   若给定的key已经存在,则setnx不做任何动作
   setnx事set if not exists (如果不存在,则set)的简写
需要注意一个点:
   只用setnx还不行,需要设置过期时间,防止系统挂掉后死锁
   加锁:set key value nx ex 10s   
   释放锁:delete key

4、redis做分布式死锁情况,如何解决

   情况1:加锁,没有释放锁。需要加释放锁的操作。比如delete key
   情况2:加锁后,程序还没有执行释放锁,程序挂了。需要用的key的过期机制

5、redis如何做分布式锁

假设有两个服务A、B都希望获得锁,执行过程大致如下:
   Step1:服务A为了获得锁,向redis发起如下命令:set productId oxx9p03001 nx ex 30000其中,productId由自己定义,可以是与本次业务有关的id,”oxx9p03001”是一串随机值,必须保证全局唯一,nx指的是当且仅当key(也就是案例中的productId:lock)在redis中不存在时,返回执行成功,否则执行失败。EX 30000指的是在30秒后,key将被自动删除。执行命令后返回成功,表明服务成功的获得了锁。
   Step2:服务B为了获得锁,向redis发起同样的命令:set productId:lock 0000111 nx ex 30000由于redis内已经存在同名key,且并未过期,因此命令执行失败,服务B未能获得锁。服务B进入循环请求状态,比如每隔1秒钟(自行设置)向redis发送请求,直到执行成功并获得锁。
   Step3:服务A的业务代码执行时长超过了30秒,导致key超时,因此redis自动删除了key。此时服务b再次发送命令执行成功,假设本次请求中设置的value值为0000222.此时需要在服务A中对key进行续期
   Step4:服务A执行完毕,为了释放锁,服务A会自动向redis发起删除key的请求。注意:在删除key之前,一定要判断服务A持有的value与redis内存储的value是否一致。比如当前场景下,redis中的锁早就不是服务A持有的那一把了,而是由服务2创建,如果贸然使用服务A持有的key来删除锁,则会误将服务2的锁释放掉。此外,由于删除锁时涉及到一系列判断逻辑,因此一般使用lua脚本,具体如下:

if redis.call(“get”, KEYS[1]) == ARGV[1] then
    return redis.call(“del”, KEYS[1])
else
    return 0
end

6、基于ZooKeeper的分布式锁实现原理

顺序节点特性:
   使用Zookeeper的顺序节点特性,假如我们在/lock/目录下创建3个节点,ZK集群按照发起创建的顺序来创建节点,节点分别为/lock/0000000001、/lock/0000000002、/lock/0000000003, 最后一位数是依次递增的,节点名由zk来完成。
临时节点特性:
   ZK中还有一种名为临时节点的节点,临时节点由某个客户端创建。当客户端与ZK集群断开连接,则该节点自动被删除,EPHEMERAL_SEQUENTIAL为临时顺序节点。

根据ZK中节点是否存在,可以作为分布式锁的锁状态,以此来实现一个分布式锁,下面是分布式锁的基本逻辑:
   1、客户端1调用create()方法创建名”/业务ID/lock-”的临时顺序节点。
   2、客户端1调用getChildren(“业务ID”)方法来获取所有已经创建的子节点。
   3、客户端获取到所有子节点path之后,如果发现自己在步骤1中创建的节点是所有节点中序号最小的,就是看自己创建的序列号是否拍第一,如果是第一,那么就认为这个客户端获取了锁,在它前面没有别的客户端拿到锁。
   4、如果创建的节点不是所有节点中需要最小的,那么则监视比自己创建节点的序列号小的最大的节点,进入等待。直到下次监听的子节点变更的时候,在进行子节点的获取,判断是否获取锁。

7、Zookeeper和redis做分布式锁的区别

redis:
  1、redis只保证最终一致性,副本间的数量复制是异步进行(set是写,get是读,redis集群一般是读写分离架构,存在主从同步延迟情况),主从切换之后可能有部分数据没有复制过去可能会【丢失锁】情况,故强一致性要求的业务不推荐使用redis,推荐使用zk。
  2、redis集群各方法的响应时间均为最低。随着并发量和业务量的提升其响应时间会明显上升(公网集群影响因素偏大),但是极限qps可以达到最大基本无异常
Zookeeper:
   1、使用Zookeeper集群,锁原理是使用Zookeeper的临时顺序节点,临时顺序节点的生命周期在client与集群的Session结束时结束。因此如果某个Client节点存在网络问题,与Zookeeper集群断开连接,Session超时同样会导致锁被错误的释放(导致被其他线程错误的持有),因此Zookeeper也无法保证完全一致。
   2、ZK具有较好的稳定性,响应时间抖动很小,没有出现异常。但是随着并发量和业务数量的提升,其响应时间和qps会明显下降。
总结:
   1、Zookeeper每次进行锁操作前都要创建若干节点,完成后要释放节点,会浪费很多时间
   2、而redis只是简单的数据操作,没有这个问题。

8、MySql如何做分布式锁

   在mysql中创建一张表,设置一个主键或者UNIQUE KEY这个KEY就是要锁的KEY,所以同一个KEY在mysql表里只能插入一次了,这样对锁的竞争就交给了数据库,处理同一个KEY数据库保证了只有一个节点能插入成功。
其他节点都会插入失败。
   DB分布式锁的实现:通过主键id或者唯一索引的唯一性进行加锁,说白了就是加锁的形式是向一张表中插入一条数据,该条数据的id就是一把分布式锁,例如当一次请求插入了一条id为1的数据,其他想要进行插入数据的并发请求必须等第一次请求执行完成后删除这条id为1的数据才能继续插入,实现了分布式锁的功能。
这样lock和unlock的思路就很简单了,伪代码:

def lock :
    exec sql : insert into locked - table (xxx) values (xxx)
    if result == true :
        return true
    else :
        return false

def unlock :
    exec sql : delete from lockedOrder where order_id = ‘order_id’

9、计数器算法是什么

   计数器算法,是指在指定的时间周期内累加访问次数,达到设定的阈值时,触发限流策略。下一个时间周期进行访问时,访问次数清零。此算法无论在单机还是分布式环境下实现都非常简单,使用redis的incr原子自增性,再结合key的过期时间,即可轻松实现。

image.png
   从上图我们来看,我们设置了一分钟的阈值是100,在0:00到1:00内请求数是60,当到1:00时,请求数清零,从0开始计算,这时在1:00到2:00之间我们能处理的最大的请求为100,超过100个的请求,系统都拒绝。
   这个算法有一个临界问题,比如在上图中,在0:00到1:00内,只在0:50有60个请求,而在1:00到2:00之间,只在1:10有60个请求,虽然在两个一分钟的时间内,都没有超过100个请求,但是在0:50到1:10这20秒内,确有120个请求,虽然在每个周期内,都没超过阈值,但是在这20秒内,已经远远超过了我们原来设置的1分钟内100个请求的阈值。

10、滑动时间窗口算法是什么?

   为了解决计数器算法的临界值问题,发明了滑动窗口算法。在TCP网络通信协议中,就采用滑动时间窗口算法来解决网络拥堵算法。
   滑动时间窗口是将计数器算法中的实际周期切分成多个小的时间窗口,分别在每个小的时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的小时间窗口的总的请求数即可
image.png
   在上图中,假设我们设置一分钟的请求阈值是100,我们将一分钟拆分成4个小时间窗口,这样,每个小的时间窗口只能处理25个请求,我们用虚线方框表示滑动的时间窗口,当前窗口的大小是2,也就是在窗口内最多能处理50个请求,随着时间的推移,滑动窗口也随着时间往前移动,比如上图开始时,窗口是0:00到0:30的这个范围,过了15秒后,窗口是0:15到0:45的这个范围,窗口中的请求重新清零,这样就很好的解决了计数器算法的临界值问题。
   在滑动时间窗口算法中,我们的小窗口划分的越多,滑动窗口的滚动就越平滑,限流的统计就会越精确。

11、漏桶限流算法

   漏桶算法的原理就像它的名字一样,我们维持一个漏斗,它有恒定的流出速度,不管水流流入的速度有多快,漏斗出水的速度始终保持不变,类似于消息中间件,不管消息的生产者请求量有多大,消息的处理能力取决于消费者。

12、设计微服务遵循的原则

  1、单一职责原则:让每个服务能独立,有界限的工作,每个服务只关注自己的业务。做到高内聚
  2、服务自治原则:每个服务能做到独立开发,独立测试、独立构建、独立部署,独立运行。与其他服务进行解耦。
  3、轻量级通信原则:让每个服务之间的调用是轻量级,并且能够跨平台、跨语言。比如采用RESTful风格,利用消息队列进行通信等。
  4、粒度进化原则:对每个服务的粒度把控,其实没有统一的标准,这个得结合我们解决得具体业务问题。不要过度设计。服务得粒度随着业务和用户的发展而发展。

13、CAP定理

概念解释:
   C:一致性,数据在多个副本节点中保持一致,可以理解成两个用户访问两个系统A和B,当A系统数据有变化时,及时同步给B系统,让两个用户看到的数据时一致的。
   A:可用性, 系统对外提供服务必须一直处于可用状态,在任何故障下,客户端都能在合理时间内获得服务端非错误的响应。
   P:分区容错性,在分布式系统中遇到任何网络分区故障,系统仍然能对外提供服务。网络分区,可以这样理解,在分布式系统中,不同的节点分布在不同的子网络中,有可能子网络中只有一个节点,在所有网络正常的情况下,由于某些原因导致这些子节点之间的网络出现故障,造成整个节点环境被分成了不同的独立区域,这就是网络分区。
   CAP定理:指的是,一个分布式系统最多只能同时满足上面三项中的两项

14、base理论

   由于CAP中一致性C和可用性A无法兼得,eBay的架构师,提出了BASE理论,它是通过牺牲数据的强一致性,来获得可用性,它由于如下3种特性:
   1、基本可用:分布式系统在出现不可预知故障的时候,允许损失部分可用性,保证核心功能的可用
   2、软状态:软状态也称为弱状态,和硬状态相对,、是允许系统种的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
   3、最终一致性:最终一致性强调的是系统种所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。因此,最总一致的本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
   BASE理论并没有要求数据的强一致性,而是允许数据在一定的时间段内是不一致的,但在最终某个状态会达到一致。在生产环境中,很多公司,会采用BASE理论来实现数据的一致,因为产品的可用性相比强一致性来说,更加重要,比如在电商平台中,当用户对一个订单发起支付时,往往会调用第三方支付平台,比如支付宝支付或者微信支付,调用第三方成功后,第三方并不能及时通知我方系统,在第三方没有通知我方系统的这段时间内,我们给用户的订单状态显示支付中,等到第三方回调之后,我们再将状态改成已支付,虽然订单状态在短期内存在不一致,但是用户却获得了更好的产品体验。

15、2PC提交协议

   二阶段提交是指,在计算网络以及数据库领域内,为了便基于分布式系统架构下的所有节点在进行事务提交时保持一致性而设计的一种算法。通常,二阶段提交也被称为是一种协议。在分布式系统中,每个节点虽然可以知晓自己的操作是成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情况决定各参与者是否要提交操作,还是中止操作。

  所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)

准备阶段

  事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种万事俱备只欠东风的状态。

可以进一步将准备阶段分为以下三个步骤:

   1)协调者节点向所有参与者节点询问是否可以执行提交操作,并开始等待各参与者节点的响应。
   2)参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
   3)各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个统一消息;如果参与者节点的事务操作实际执行失败,则它返回一个中止消息。

提交/回滚阶段

  如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚消息;否则,发送提交消息;
  参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源(注意:必须在最后阶段释放锁资源)

接下来分两种情况分别讨论提交阶段的过程。

当协调者节点从所有参与者节点获得的相应消息都为同意时
   1)协调者节点向所有参与者节点发出”正式提交的请求”。
   2)参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
   3)参与者节点向协调者节点发送完成消息
   4)协调者节点受到所有参与者节点反馈的完成消息后,完成事务
如果任一参与者节点在第一阶段返回的相应消息为中止,或者协调者节点在第一阶段的时间超过之前无法获取所有参与者节点的响应消息时
   1)协调者节点向所有参与者节点发出回滚操作的请求
   2)参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源
   3)参与者节点向协调者节点发送回滚完成消息
   4)协调者节点受到所有参与者节点反馈的回滚完成消息后,取消事务
不管最后结果如何,第二阶段都会结束当前事务

16、两阶段提交协议有哪些缺点

   1、同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态
   2、单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
   3、数据不一致。在二阶段提交的阶段中,当协调者向参与者发生请求之后,发生了局部网络异常或者在发生过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作,但是其他部分未接到commit请求的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致的现象。
   4、二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即便协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交
   处理方法:补偿。手动补偿,脚本补偿

17、3pc提交协议是什么

CanCommit阶段
3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发生commit请求,参与者如果可以提交就返回yes响应,否则返回no响应
  1、事务询问:协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
   2、响应反馈:参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回yes响应,并进入预备状态。否则反馈no
PreCommit阶段
   协调者根据参与者的反应情况来决定是否可以进行事务的PreCommit操作。根据响应情况,有以下两种可能
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

   1、发送预提交请求:协调者向参与者发送PreCommit请求,并进入Prepared阶段
   2、事务预提交:参与者接收到PreCommit请求后,会执行事务操作,并将Undo和redo信息记录到事务日志
   3、响应反馈: 如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令
假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。
   1、发送中断请求: 协调者向所有参与者发送abort请求
   2、中断事务: 参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断
   pre阶段参与者没收到请求,rollback
doCommit阶段
   该阶段进行真正的事务提交,也可以分为以下两种情况
执行提交
   1、发送提交请求 协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态,并向所有参与者发送doCommit请求
   2、事务提交 参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源
   3、响应反馈 事务提交完之后向协调者发送Ack响应
   4、完成事务 协调者接收到所有参与者的ack响应之后,完成事务
   中断事务: 协调者没有接收到参与者发送的ACK响应(可能是接收者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务
   1、发送中断请求 协调者向所有参与者发送abort请求
   2、事务回滚 参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源
   3、反馈结果 参与者完成事务回滚之后,向协调者发送ACK消息
   4、中断事务 协调者接收到参与者反馈的ACK消息之后,执行事务的中断

18、2PC和3PC的区别是什么

   1、3pc比2pc多了一个can commit阶段,减少了不必要的资源浪费。因为2pc在第一阶段会占用资源,而3pc在这个阶段不占用资源,只是校验一下sql,如果不能执行,就直接返回,减少了资源占用。
  2、引入超时机制。同时在协调者和参与者中都引入超时机制
    (1) 2pc:只有协调者有超时机制,超时后,发送回滚指令
    (2) 3pc:协调者和参与者都有超时机制
  协调者超时:发送中断指令。can commit,pre commit中,如果收不到参与者的反馈,则协调者向参与者发送中断指令
   参与者超时:pre阶段进行中断,do阶段进行提交。pre commit阶段,参与者进行中断;do commit阶段,参与者进行提交

19、TCC解决方案是什么

TCC是一种常用的分布式事务解决方案,它将一个事务拆分成三个步骤:
   T:业务检查阶段,这阶段主要进行业务校验和检查或者资源预留;也可能是直接进行业务操作。
   C:业务检查阶段,这阶段对Try阶段校验过的业务或者预留的资源进行确认
   C:业务回滚阶段,这阶段和上面的C是互斥的,用于释放Try阶段预留的资源或者业务

20、TCC空回滚是解决什么问题的?

   在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法。比如当Try请求由于网络延迟或故障等原因,没有执行,结果返回了异常,那么此时Canale就不能正常执行,因为Try没有对数据进行修改,如果Cancel进行了数据的而修改,那就会导致数据不一致。
   解决思路是关键就是要识别出这个空回滚,思路很简单就是需要知道Try阶段是否执行,如果执行了,那就是正常回滚,如果没有执行,那就是空回滚。建议TM在发起全局事务时生成全局事务记录,全局事务id贯穿整个分布式事务调用链条。再额外增加一张分支事务记录表,其中有全局事务id和分支事务id,第一阶段Try方法里会插入一条记录,表示Try阶段执行了。Cancel接口里读取该记录,如果该记录存在,则正常回滚,如果该记录不存在,则是空回滚。

21、如何解决TCC幂等问题

   为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC的二阶段confirm和cancel接口保证幂等,这样不会重复使用或者释放资源。如果幂等控制没有做好,很可能导致数据不一致等严重问题。
   解决思路在上述分支事务记录中增加执行状态,每次执行前都查询该状态。

22、如何解决TCC中悬挂问题

   悬挂就是对于一个分布式事务,其二阶段Cancel接口比Try接口先执行。
   出现原因是在调用分支事务Try时,由于网络发生拥堵,造成了超时,TM就会通知RM回滚该分布式事务,可能回滚完成后,Try请求才到达参与者真正执行,而一个Try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源就再也没有人能够处理了,对于这种情况,我们称为悬挂,即业务资源预留后无法继续处理。
   解决思路是如果二阶段执行完成,那一阶段就不能再继续执行。在执行一阶段事务时判断在该全局事务下,判断分支事务记录表中是否已经有二阶段事务记录,如果有则不执行Try。

23、可靠消息服务方案是什么

   可靠消息最终一致性方案指的是:当事务的发起方(事务参与者,消息发送者)执行完本地事务后,同时发出一条消息,事务参与方(事务参与者,消息的消费者)一定能够接收消息并可以成功处理自己的事务。
这里面强调两点:
   1、可靠消息:发起方一定得把消息传递到消费者。
   2、最终一致性:最终发起方的业务处理和消费方的业务处理得完成,达成最终一致。

24、最大努力通知方案得关键事什么

   1、有一定的消息重复通知机制。因为接收通知方可能没有接收到通知,此时要有一定的机制对消息重复通知。
   2、消息校对机制。如果尽最大努力也没有通知到接收方,或者接收方消费消息后要再次消费,此时可由接收方主动向通知方查询消息信息来满足需求。

25、什么是分布式系统中的幂等

   幂等是一个数学与计算机学概念,常见于抽象代数中。
   在编程中,一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数,这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。
   例如:getUsername()和setTrue()函数就是一个幂等函数,更复杂的操作幂等保证是利用唯一交易号(流水号)实现。我的理解:幂等就是一个操作,不论执行多少次,产生的效果和返回的结果都是一样的。

26、幂等有哪些技术解决方案

1、查询操作

   查询一次和查询多次,在数据不变的情况下,查询结果是一样的,select是天然的幂等操作;

2、删除操作

   删除操作也是幂等的,删除一次和多次删除都是吧数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个)

3、唯一索引

   防止新增脏数据,比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建多个资金账户,那么给资金账户表中的用户id加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可)

4、token机制

防止页面重复提交。

   业务要求:页面的数据只能被点击提交一次
   发生原因:由于重复点击或者网络重发,或者nginx重发等情况会导致数据被重复提交;
   解决办法:集群环境采用token加redis(redis单线程的,处理需要排队);单JVM环境:采用token加redis或token加jvm锁。

处理流程:

   1、数据提交前要向服务的申请token,token放到redis或jvm内存,token有效时间;
   2、提交后后台校验token,同时删除token,生成新的token返回。
token特点:要申请,一次有效性,可以限流。
   注意:redis要用删除操作来判断token,删除成功代表token校验通过。

5、traceId

   操作时唯一的。

27、对外提供的API如何保证幂等

   举例说明:银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号。
   source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求)。
   重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。
   注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理。

28、双写一致性问题如何解决

   先做一个说明,从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。这种方案下,我们可以对存入缓存的数据设置过期时间,所有的写操作以数据库为准,对缓存操作只是尽最大努力更新即可。也就是说如果数据库写成功,缓存更新失败,那么只要到达过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
   因此,接下来讨论的思路不依赖于给缓存设置过期时间这个方案。
在这里,我们讨论三种更新策略:
   1、先更新缓存,再更新数据库。(不可取)
   2、先更新数据库,再更新缓存。(不可取)
   3、先删除缓存,再更新数据库。(不可取)
   4、先更新数据库,再删除缓存。(可取,有问题待解决)

大前提:

   先读缓存,如果缓存没有,才从数据库读取。

(1)先更新数据库,再更新缓存

   这套方案,大家是普遍反对的。为什么呢?有以下两点原因。

原因一(线程安全角度)

同时有请求A和请求B进行更新操作,那么会出现
   (1)线程A更新了数据库
   (2)线程B更新了数据库
   (3)线程B更新了缓存
   (4)线程A更新了缓存
   这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存,这就导致脏数据,因此不考虑。

原因二(业务场景角度)

有如下两点:
   1、如果你是一个写数据库场景比较多,而读数据场景比较少的业务需求,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
   2、如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。显然,删除缓存更为适合。
   接下来讨论的就是争议最大的,先删缓存,再更新数据库,还是先更新数据库,再删缓存的问题

(2)先删缓存,再更新数据库

   该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作,那么会出现如下情形:
  (1)请求A进行写操作,删除缓存
  (2)请求B查询发现缓存不存在
  (3)请求B去数据库查询得到旧值
  (4)请求B将旧值写入缓存
  (5)请求A将新值写入数据库
   上述情况就会导致不一致的情形出现,而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

如何解决呢上面问题呢?采用延时双删策略

   (1)先淘汰缓存
   (2)再写数据库(这两步和原来一样)
   (3)休眠一秒,再次淘汰缓存
   这么做,可以将一秒内所造成的缓存脏数据,再次删除。

那么,这个1秒怎么确定的,具体该休眠多久呢?

   针对上面的情形,读者应该自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则再读数据业务逻辑的耗时基础上,加几百ms即可,这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

如果你用了mysql的读写分离架构怎么办?

   ok,这种情况下,造成数据不一致的原因如下,还是两个请求,一个请求A进行更新操作,另一个请求B进行查询操作。
   (1)请求A进行写操作,删除缓存
   (2)请求A将数据写入数据库了
   (3)请求B查询缓存发现,缓存没有值
   (4)请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
   (5)请求B将旧值写入缓存
   (6)数据库完成主从同步,从库变为新值
   上述情形,就是数据下一致的原因。还是使用双删延时策略。只是,睡眠时间修改为在主从同步的延时时间基础上,加几百ms。

采用这种同步淘汰策略,吞吐量降低怎么办?

   ok,那就将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用沉睡一段时间后了,再返回。这么做,加大吞吐量。

第二次删除,如果删除失败怎么办?

  这是个非常好的问题。因为第二次删除失败,就会出现如下情形。还是有两个请求,一个请求A进行更新操作,另一个请求B进行查询操作,为了方便,假设是单库:
  (1)请求A进行写操作,删除缓存
  (2)请求B查询发现缓存不存在
  (3)请求B去数据库查询得到旧值
  (4)请求B将旧值写入缓存
  (5)请求A将新值写入数据库
  (6)请求A试图去删除,请求B写入对的缓存值,结果失败了
   ok,这也就是说。如果第二次删除缓存失败,会再次出现缓存和数据库不一致的问题。

如何解决呢

(3)先更新数据库,再删缓存
   首先,先说一下。老外提出了一个缓存更新的套路。其中就指出
   失效: 应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中
   命中: 应用程序从cache中取数据,取到后返回
   更新: 先把数据库存到数据库中,成功后,再让缓存失效
这种情况不存在并发吗?
   不是的,假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
   (1)缓存刚好失效
   (2)请求A查询数据库,得一个旧值
   (3)请求B将新值写入数据库
   (4)请求B删除缓存
   (5)请求A将查到得旧值写入缓存
   ok,如果发生上述情况,确实是会发生脏数据
然而,发生这种情况得概率会有多少呢?    发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。可是,大家想想,数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
   假设,有人非要抬杠,有强迫症,一定要解决怎么办?
如何解决上述并发问题?
   首先,给缓存设有效时间是一种方案。其次,采用策略(2)里给出的异步延时删除策略,保证读请求完成之后,再进行删除操作。
还有其他造成不一致的原因么?
   有的,这也是缓存更新策略(2)和缓存更新策略(3)都存在的一个问题,如果删缓存失败了怎么办,那不是会有不一致的情况出现么。比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况了。这也是缓存更新策略(2)里留下的最后一个疑问。
如何解决?
   提供一个保障的重试机制即可,这里给出两套方案。
方案一:
代码耦合度比较高,一般会用方案二
   (1)更新数据库数据
   (2)缓存因为种种问题删除失效
   (3)将需要删除的key发生至消息队列
   (4)自己消费消息,并获得需要删除的key
   (5)继续重试删除操作,直到成功
   然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案二:
流程如下图所示:
   (1)更新数据库数据
   (2)数据库会将操作信息写入binlog日志当中
   (3)订阅程序提取出所需要的数据以及key
   (4)另起一段非业务代码,获得该信息
   (5)尝试删除缓存操作,发现删除失败
   (6)将这些信息发生至消息队列
   (7)重新从消息队列中获得该数据,重试操作

29、分布式微服务项目是如何设计的

   我一般设计成两层:业务层和能力层(中台),业务层接受用户请求,然后通过调用能力层来完成业务逻辑。

30、认证和授权的区别

   Authentication(认证)是验证您的身份的凭据(例如用户名/用户ID和密码),通过这个凭据,系统得以知道你就是你,也就是说系统存在你这个用户。所以,Authentication被称为身份/用户验证
   Authorization(授权)发生在Authentication(认证)之后。授权,它主要掌管我们访问系统的权限。比如有些特定资源只能具有特定权限的人才能访问比如admin,有些对系统资源操作比如删除、添加、更新只能特定人才具有。
   这两个一般在我们的系统中被结合在一起使用,目的就是为了保护我们系统的安全性。

31、Cookie和Session有什么区别,使用session进行身份验证

   Session的主要作用就是通过服务端记录用户的状态。
   Cookie数据保存在客户端(浏览器端),session数据保存在服务器端。相对来说session安全性更高。如果使用Cookie的话,一些敏感信息不要写入Cookie中,最好能将Cookie信息加密然后使用到的时候再去服务器端解密。

那么,如何使用session进行身份验证?

   很多时候我们都是通过SessionID来指定特定的用户,SessionID一般会选择存放在服务端。举个例子:用户成功登录系统,然后返回给客户端具有SessionID的cookie,当用户向后端发起请求的时候会把SessionID带上,这样后端就知道你的身份状态了。

session详细的过程如下:

   用户向服务器发送用户名和密码用于登录系统
   服务器验证通过后,服务器为用户创建一个session,并将session信息存储起来。
   服务器向用户返回一个sessionID,写入用户的cookie
   当用户保持登录状态时,cookie将与每个后续请求一起被发送出去。
   服务器可以将存储在cookie上的sessionID与存储在内存中或者数据库中的session信息进行比较,以验证用户的身份,返回给用户客户端响应信息的时候会附带用户当前的状态。

使用session的时候需要注意下面几个点:

   1、依赖session的关键业务一定要确保客户端开启了cookie。
   2、注意session的过期时间

32、什么是Token?什么是JWT?如何基于Token进行身份验证?

   我们知道session信息需要保存一份在服务器端。这种方式会带来一些麻烦,比如需要我们保证保存session信息服务器的可用性、不适合移动端(不依赖cookie)等。
   有没有一种不需要自己存放session信息就能实现身份验证的方式呢?使用Token即可!JWT就是这种方式的实现,通过这种方式服务器端就不需要保存session数据了,只用在客户端保存发服务端返回给客户的Token就可以了,扩展性得到提升。
   JWT本质上是一段签名的JSON格式的数据。由于它是带着签名的,因此接收者便可以验证它的真实性。
JWT由3部分构成:
   Header:描述JWT的元数据。定义了生成签名的算法以及Token的类型。
   Payload(负载):用来存放实际需要传递的数据
   Signature(签名):服务器通过Payload、Header和一个密钥(secret)使用Header里面指定的签名算法(默认是HMAC SHA256)生成。
   在基于Token进行身份验证的应用程序中,服务器通过Payload、Header和一个密钥(secret)创建令牌。(Token)并将Token发送给客户端,客户端将Token保存在cookie或者localStorage里面,以后客户端发出的所有请求都会携带这个令牌。你可以把它放在cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在HTTP Header的Authorization字段中:Authorization:Bearer Token。

  用户向服务器发送用户名和密码用于登录系统。
  身份验证服务响应并返回了签名的JWT,上面包含了用户是谁的内容。
  用户以后每次向后端发请求都在Header中带上JWT。
  服务端检查JWT并从中获取用户相关信息。

33、为什么Cookie无法防止CSRF攻击,而token可以?

   CSRF一般被翻译为跨站请求伪造。那么什么是跨站请求伪造呢?说简单一点用你的身份去发送一些对你不友好的请求。举个简单的例子:
   小壮登录了某网上银行,它来到了网上银行的帖子区,看到一个帖子下面有一个链接写着”科学理财,年收益率70%”,小壮好奇的点开了这个链接,结果发现自己的账户少了10000元。这是怎么回事呢?原来黑客在链接中长着一个请求,这个请求直接利用小壮的身份给银行发送了一个转账请求,也就是通过你的cookie向银行发送请求。
   原因是进行session认证的时候,我们一般使用cookie来存储sessionID,当我们登陆后后端生成一个sessionID放在cookie中返回给客户端,服务端通过redis或者其他存储工具记录保存着这个sessionID,客户端登录以后每次请求都会带上这个sessionID,服务端通过这个sessionID来标示你这个人。如果别人通过cookie拿到了sessionID后就可以代替你的身份访问系统了。
   session认证中cookie中的sessionID是由浏览器发送到服务端的,借助这个特性,攻击者就可以通过让用户误点攻击链接,达到攻击效果。
   但是,我们使用token的话就不会存在这个问题,在我们登录成功获得token之后,一般会选择存放在localstorage中。然后我们在前端通过某些方式会给每个发到后端的请求加上这个token,这样就不会出现CRSF漏洞的问题。因为,即使有个你点击了非法链接发送了请求到服务端,这个非法请求是不会携带token的,所以这个请求将是非法的。

分布式两大难题
   1、分布式锁
   2、分布式事务

分布式锁实现方案:
   mysql、redis、zookeeper

分布式事务解决方案:
   lcn、seata、消息中间件+事件表,可靠消息服务

34、分布式架构下,session共享有什么方案

  1、不要session:但是确实在某些场景下,是可以没有session的,其实在很多接口类系统中,都提倡【API无状态服务】;也就是每一次的接口访问,都不依赖于session、不依赖于前一次的接口访问,用jwt的token。
  2、存入cookie中:将session存储到cookie中,但是缺点也很明显,例如每次请求都得带着session,数据存储在客户端本地,是有风险的。
  3、session同步:对各个服务器之间同步session,这样可以保证每个服务器上都有全部的session信息,不同当服务器数量比较多的时候,同步是会有延迟甚至同步失败;(一般没人使用)
  4、我们现在的系统会把session放到redis中存储,虽然架构上变得复杂,并且需要访问多访问一次redis,但是这种方案带来的好处也是很大的:实现session共享,可以水平扩展(增加redis服务器),服务器重启session不丢失(不过也要注意session在redis中的刷新/失效机制),不仅可以跨服务器session共享,甚至可以跨平台(例如网页端和APP端)进行共享。
  5、使用nginx(或其他复杂均衡软硬件)中的ip绑定策略,同一个ip只能在指定的同一个机器访问,但是这样做风险也比较大,而且也是去了负载均衡的意义:不可取

35、系统如何做高可用、高并发,高扩展系统

设计三高系统设计原则:

4要1不要
数据要少,请求数要少,路径要短,依赖要少
不要单点

处理热点数据。

  通过uv,pv提前识别访问到系统上的这些值
  提前业务上的处理,添加购物车,如果添加购物车的数量够多,说明这是热点数据
  优化、限制(消息队列)、隔离(系统垮掉不影响别人)

隔离方式:

  有30个商品、2台redis,热点商品用一个redis,29个商品用另外一台redis

削峰:

1、排队:1001个人访问的时候,让他们排队等着
2、分层过滤:
3、流量评估 qps,tps

稳(高可用),准(数据一致性),快(高性能)。
高可用: 做负载均衡
数据一致性: 分布式事务,分布式锁
快: 提高cpu性能,或者减少线程的等待时间

发现系统性能的瓶颈:

减少io
在代码中减少序列化
并发读的优化:读缓存

数据一致性问题:

防止超卖和少卖
少买:线程拿到锁执行业务,拿不到锁就直接返回
防止少卖,要么重试,要么阻塞。队列

少买和超卖的问题都出在锁的处理上

事务:

系统一:

请求购买   订单服务     DB库
   积分服务 DB库
   事务就是为了满足他们两同时成功或失败。他们是互不相认的,为了保证他们同时失败或成功,需要第三方协调(事务协调器)。

假如第三方决定两个都提交,但这时一个服务器挂掉了怎么办?
1、重试
2、补偿:会写脚本 image.png