每秒查询率 ,是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准, 即每秒的响应请求数,也即是最大吞吐能力。
事务数/秒。一个事务是指一个客户机向服务器发送请求(含事务的操作)然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,
指系统同时能处理的请求数量,同样反应了系统的负载能力。这个数值可以分析机器1s内的访问日志数量来得到
白话文:常常说的并发高,并发高,说的就是接口的请求数量多(准确来说是系统,不止单个接口哈)
指系统在单位时间内处理请求的数量,TPS、QPS都是吞吐量的常用量化指标。
白话文:一个请求对cpu的消耗、三方接口、IO响应速度都会影响吞吐量
这个和数据库ACID的一致性类似,但这里关注的所有数据节点上的数据一致性和正确性,而数据库的ACID关注的是在在一个事务内,对数据的一些约束。系统在执行过某项操作后仍然处于一致的状态。在分布式系统中,更新操作执行成功后所有的用户都应该读取到最新值。
个人理解:关注点在业务逻辑上的一致性
当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。这种是对用户最友好的,就是用户上一次写什么,下一次就保证能读到什么。根据 CAP 理论,这种实现需要牺牲可用性。
白话文:写一个值,就立马能读到什么
系统并不保证续进程或者线程的访问都会返回最新的更新过的值。用户读到某一操作对系统特定数据的更新需要一段时间,我们称这段时间为“不一致性窗口”。系统在数据写入成功之后,不承诺立即可以读到最新写入的值,也不会具体的承诺多久之后可以读到。
白话文:写一个值,但我不确定能不能读取到,也不确定什么时候能读取到(好渣呀)
是弱一致性的一种特例。系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。在没有故障发生的前提下,不一致窗口的时间主要受通信延迟,系统负载和复制副本的个数影响。DNS 是一个典型的最终一致性系统。
白话文:写一个值,如果过一段时间(这个时间依据什么?TODO)没后续的更新了,那么下次就可以读取到这个值
每一个操作总是能够在一定时间内返回结果。需要注意“一定时间”和“返回结果”。“一定时间”是指,系统结果必须在给定时间内返回。“返回结果”是指系统返回操作成功或失败的结果。
接口反馈速度越快,可用性越高;还要保证一定能返回数据
是否可以对数据进行分区。这是考虑到性能和可伸缩性。
这个一般情况下都是满足的,所以基本都是CP、AP
就是说一个接口,多次发起同一个请求,如何这个接口得保证结果是准确的,比如不能多扣款、不能多插入一条数据、不能将统计值多加了 1。
白话文:同一接口多次请求,保证结果是对的(一般是更新操作,查询当然是一样的)
假如你有个服务提供一些接口供外部调用,这个服务部署在了 5 台机器上,接着有个接口就是付款接口。然后人家用户在前端上操作的时候,不知道为啥,总之就是一个订单不小心发起了两次支付请求,然后这俩请求分散在了这个服务部署的不同的机器上,好了,结果一个订单扣款扣两次。
订单系统调用支付系统进行支付,结果不小心因为网络超时了,然后订单系统走了前面我们看到的那个重试机制,咔嚓给你重试了一把,好,支付系统收到一个支付请求两次,而且因为负载均衡算法落在了不同的机器上
其实保证幂等性主要是三点:
每个请求必须有一个唯一的标识,举个栗子:订单支付请求,肯定得包含订单 id,一个订单 id 最多支付一次,对吧。
每次处理完请求之后,必须有一个记录标识这个请求处理过了。常见的方案是在 mysql 中记录个状态啥的,比如支付之前记录一条这个订单的支付流水。
每次接收请求需要进行判断,判断之前是否处理过。比如说,如果有一个订单已经支付了,就已经有了一条支付流水,那么如果重复发送这个请求,则此时先插入支付流水,orderId 已经存在了,唯一键约束生效,报错插入不进去的。然后你就不用再扣款了。
白话文:1.每个请求要有唯一标识 2.数据库要有记录 3.有更新操作前先去判断一个这个请求是否处理过
利用一张日志表来记录已经处理成功的请求的ID,如果新到的请求ID已经在日志表中,那么就不再处理这个请求
value存放请求的唯一标识,当存进去的时候才能处理请求,可以用set类型实现(判断元素是否在set集合中:sismember key member)
指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
白话文:一次事务操作涉及多个服务,保证这些服务的操作一致性。
会的,我个人认为,上游采用本地事务,调用完接口后,提交了本地事务,下游成功与否感知不到。。。TODO
做系统拆分(单机拆成微服务)的时候几乎都会遇到分布式事务的问题,一个仿真的案例如下:
关键字:订单模块、钱包模块、系统拆分
项目初期,由于用户体量不大,订单模块和钱包模块共库共应用(大war包时代),模块调用可以简化为本地事务操作,这样做只要不是程序本身的BUG,基本可以避免数据不一致。后面因为用户体量越发增大,基于容错、性能、功能共享等考虑,把原来的应用拆分为订单微服务和钱包微服务,两个服务之间通过非本地调用操作(这里可以是HTTP或者消息队列等)进行数据同步,这个时候就很有可能由于异常场景出现数据不一致的情况。
订单服务调用钱包服务进行扣款操作,之后修改订单状态为已支付
以上面的订单微服务请求钱包微服务进行扣款并更新订单状态为扣款这个调用过程为例,假设采用HTTP同步调用,项目如果由经验不足的开发者开发这个逻辑,可能会出现下面的伪代码:
[开启事务]
1、查询订单
2、HTTP调用钱包微服务扣款
3、更新订单状态为扣款成功
[提交事务]
白话文:对对对,年轻的程序员都用这种方法!强一致的前提是没有出现网络波动!
问题的根本原因是HTTP调用存在网络等问题
由于钱包微服务本身各种原因导致扣款接口响应极慢,会导致上面的处理方法事务(准确来说是数据库连接)长时间挂起,持有的数据库连接无法释放,会导致数据库连接池的连接耗尽,很容易导致订单微服务的其他依赖数据库的接口无法响应
白话文:HTTP调用时间太久,延长了事务的时间,占用数据库连接太久,其他接口有意见
钱包微服务是单节点部署(并不是所有的公司微服务都做得很完善),升级期间应用停机,上面方法中第2步接口调用直接失败,这样会导致短时间内所有的事务都回滚,相当于订单微服务的扣款入口是不可用的。
白话文:下游在部署中,导致订单事务频繁失败回滚,功能直接不能用了
网络是不可靠的,HTTP调用或者接受响应的时候如果出现网络闪断有可能出现了服务间状态不能互相明确的情况,例如订单微服务调用钱包微服务成功,接受响应的时候出现网络问题,会出现扣款成功但是订单状态没有更新的可能(订单微服务事务回滚)。
白话文:钱包服务扣款成功,因为网络问题,没能告诉订单服务,订单服务回滚(白付钱了)
这种解决方案,只能每天祈求下游服务或者网络不出现任何问题
使用消息队列进行服务之间的调用也是常见的方式之一,但是使用消息队列交互本质是异步的,无法感知下游消息消费方是否正常处理消息。假设采用消息队列异步调用,项目如果由经验不足的开发者开发这个逻辑,可能会出现下面的伪代码
同步响应太慢,那我用消息队列异步去执行任务,总快吧,但是下游服务能不能处理好消息我就不管了
[开启事务]
1、查询订单
2、推送钱包微服务扣款消息(消息队列实现推送消息)
3、更新订单状态为扣款成功
[提交事务]
这样做,在正常情况下,也就是能够正常调用消息队列中间件且推送消息成功的情况下,事务是能够正确提交的。但是存在两个明显的问题:
消息队列中间件出现了异常,无法正常调用,常见的情况是网络原因或者消息队列中间件不可用,会导致异常从而使得事务回滚。这种情况看起来似乎合情合理,但是仔细想:为什么消息队列中间件调用异常会导致业务事务回滚,如果中间件不恢复,这个接口调用岂不是相当于不可用?
=>白话文:消息队列炸了,消息都推不过去,导致一直回滚,导致接口不可用
就祈祷下游不会出现问题吧
如果消息队列中间件正常,消息推送正常,但是之后由于下游SQL存在语法错误导致事务回滚,这样就会出现了下游微服务被调用成功,本地事务却回滚的问题,导致了上下游系统数据不一致。(这个问题我在做项目的时候遇到过!是采用完成本地事务之后再推送数据解决的,调整了一下代码顺序,其实还是有问题的,要保证下游不会出现问题才行)
白话文:异步推送成功,但推送之后代码出问题导致本地事务回滚,没办法通知下游服务一起回滚
事务中进行异步消息推送是一种并不可靠的实现
当然!RocketMQ除外!别急别急,下面会细说的
来看看业内的大佬们是如何解决的
原文链接:多阶段提交方案(2PC、3PC)
原文链接:面试必问:分布式事务六种解决方案
业界目前主流的分布式事务解决方案主要有:多阶段提交方案(2PC、3PC)、补偿事务和消息事务(主要是RocketMQ,基本思想也是多阶段提交方案,其他消息队列中间件并没有实现分布式事务
常见的有二阶段和三阶段提交事务,需要额外的资源管理器来协调事务,数据一致性强,但是实现方案比较复杂,对性能的牺牲比较大(主要是需要对资源锁定,等待所有事务提交才能解锁),不适用于高并发的场景,目前比较知名的有阿里开源的fescar。
白话文:对操作的所有资源锁定,强一致性,并发就低了
关键字:协调者、事务参与者、询问
事务管理器作为全局的调度者,负责各个本地资源的提交和回滚
本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口
协议中假设每个节点都会记录写前日志(write-ahead log)并持久性存储(如mysql中的redolog和undolog),即使节点发生故障日志也不会丢失。
协议中同时假设节点不会发生永久性故障而且任意两个节点都可以互相通信。
白话文: 2个假设,1.写之前记录日志并持久化(用于回滚);2.服务器不会炸且没有网络问题
XA实现分布式事务的原理如下:
图片来源:https://www.cnblogs.com/cxxjohnson/p/9145548.html
白话文:上图可知事务管理器又叫协调者,本地资源管理器又叫事务参与者
两阶段提交协议是协调所有分布式原子事务参与者,并决定提交或取消(回滚)的分布式算法。
吐槽:为啥叫协调这么难记,事务参与者会打架吗?
在两阶段提交协议中,系统一般包含两类机器(或节点)
通常一个系统中只有一个;
一般包含多个,在数据存储系统中可以理解为数据副本的个数。
白话文:1个协调者 + 多个事务参与者
在请求阶段,协调者将通知事务参与者准备提交或取消事务,然后进入表决过程。
在表决过程中,参与者将告知协调者自己的决策:同意(事务参与者本地作业执行成功)或取消(本地作业执行故障)。
白话文:老板(协调者)组织团建,让员工表决(事务参与者),员工有(同意)也有不同意的(取消)
在该阶段,协调者将基于第一个阶段的投票结果进行决策:提交或回滚。
当且仅当所有的参与者同意提交事务协调者才通知所有的参与者提交事务,否则协调者将通知所有的参与者取消事务。
参与者在接收到协调者发来的消息后将执行响应的操作。
白话文:老板(协调者)看到所有员工都(同意),才决定去团建(提交事务),只要有一个不同意,就取消团建(回滚取消事务)
执行过程中,所有参与节点都是事务阻塞型的。
当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
事务参与者用到的资源被锁定
由于协调者的重要性,一旦协调者发生故障。
参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(如果是协调者挂掉,可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
协调者是单节点的
在执行阶段,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中事务参与者发生了故障,这回导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是部分挂掉的机器则无法执行事务提交。于是整个分布式系统便出现了数据不一致性的现象。
白话文:老板发通知的时候,刚好有部分员工睡着了没听到,然后一直在等待结果(无法执行事务提交)
关键字:超时机制、询问阶段、响应反馈
三阶段提交协议在协调者和参与者中都引入超时机制,并且把两阶段提交协议的第一个阶段拆分成了两步:询问,然后再锁资源,最后真正提交。
协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。
白话文:询问是否可以正常执行
协调者根据事务参与者的第一步反应情况来决定是否可以继续事务的PreCommit操作。
根据响应情况,有以下两种可能。
那么就会进行事务的预执行:
假如有任何一个No响应,或者等待超时之后,就中断事务:
白话文:询问好了,都OK的话,就发送预提交给事务参与者,事务参与者进行事务预提交,记录2个日志;否则,中断事务就可以(这时候不用回归)
该阶段进行真正的事务提交,也可以分为以下两种情况:
老板(协调者)通知了员工(事务参与者)确定去团建,所以员工(事务参与者)都知道了并给老板(协调者)响应(ACK响应),团建的事就敲定了(完成事务)
协调者没有接收到事务参与者发送的ACK响应(可能是发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。
老板(协调者)通知了员工(事务参与者)确定去团建,但还是有员工(事务参与者)没给老板(协调者)响应(ACK响应),就会触发中断事务
通过redo、undo日志,执行提交和回滚操作
对性能的牺牲比较大,对资源锁定,等待所有事务提交才能解锁,不适用于高并发的场景,目前比较知名的有阿里开源的Seata
需要额外的资源管理器来协调事务
想想都感觉好复杂。。。
一般也叫TCC,因为每个事务操作都需要提供三个操作尝试(Try)、确认(Confirm)和补偿/撤销(Cancel),数据一致性的强度比多阶段提交方案低,但是实现的复杂度会有所降低(低??确实),比较明显的缺陷是每个业务事务需要实现三组操作,有可能出现过多的补偿方案的代码;另外有很多场景TCC是不合适的。
白话文:TCC,有三个操作(尝试、确认、撤销),每个事务的业务都要写这三个步骤,代码开发量变多了,
tips个人理解:Try生成一个草稿(预状态),comfirm把草稿变成真的,cancel删除草稿
图片来源:https://www.cnblogs.com/jajian/p/10014145.html
主要是对业务系统做检测及资源预留,一般都是锁定某个资源,设置一个预备状态,冻结部分数据
白话文:每个服务先锁住要用的资源,之后一起执行
其实就是一个没有实际业务含义的状态,比如说修改中、数据冻结中,又或者是一个预增加的字段
主要是对业务系统做确认提交,Try阶段执行成功并开始执行Confirm阶段时,默认Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
白话文:执行业务,释放对资源的锁,返回成功
主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。
白话文:执行失败了,回滚代码,释放对资源的锁,返回失败
国内开源的 ByteTCC、Himly、TCC-transaction等
tcc分布式事务在这里起到了一个事务协调者的角色。真实业务只需要调用try阶段的方法。confirm和cancel阶段的方法由tcc框架来帮我们调用完成最终业务逻辑
图片来源:https://www.jianshu.com/p/e31d9ebed201
本地消息表其实就是利用了各系统本地的事务来实现分布式事务。
预存状态、成功状态、失败状态
比如说请求的唯一识别
白话文:1.本地创建一张消息表,将业务的操作和将消息放入消息表的操作放到同一个事务中。2.有一个定时任务,处理本地消息表的未成功的数据,不断重试重试,如果还不行就人工处理这些数据
本地消息表顾名思义就是会有一张存放本地消息的表,一般都是放在数据库中,然后在执行业务的时候将业务的执行和将消息放入消息表中的操作放在同一个事务中,这样就能保证消息放入本地表中业务肯定是执行成功的。
然后再去调用下一个操作,如果下一个操作调用成功了好说,消息表的消息状态可以直接改成已成功。
如果调用失败也没事,会有 后台任务定时去读取本地消息表,筛选出还未成功的消息再调用对应的服务,服务更新成功了再变更消息的状态。
这时候有可能消息对应的操作不成功,因此也需要重试,重试就得保证对应服务的方法是幂等的,而且一般重试会有最大次数,超过最大次数可以记录下报警让人工处理。
本地消息表实现的是最终一致性,容忍了数据暂时不一致的情况
先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见
发送成功后发送方再执行本地事务
根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令
并且 RocketMQ 的发送方会提供一个反查事务状态接口(check接口),如果一段时间内半消息没有收到任何操作请求(超时检测),那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
白话文:上游给MQ发送一个半消息,并提供一个check接口,然后去执行本地事务,check接口检测本地事务执行完成后,MQ把半消息变成真消息,下游可见可消费,下游消费失败会不断重试
本地消息表也可以算最大努力,消息事务也可以算最大努力。
就本地消息表来说会有后台任务定时去查看未完成的消息,然后去调用对应的服务,当一个消息多次调用都失败的时候可以记录下然后引入人工,或者直接舍弃。这其实算是最大努力了。
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。
它是由多个有序的事务组成、并且与其他事务可以交错的一个长时间事务(LLT:long lived transaction?)
一个长时间事务会在相对较长的时间内占用数据库资源,明显的阻碍了较短的和公用的其他事务完成。
白话文:长时间事务引发的问题
saga讲述的是如何处理long lived transaction(长活事务),一个系统中如果流程很长,如果需要保证数据库ACID特性,则事务时间很长,这样容易出现死锁错误。
对系统进行分布式拆分,形成多个子系统(子事务);由一个协调者统一管理,最终统一的提交/回滚,则可以避免事务时间过长的问题(需要注意的是,这里的提交和回滚是业务逻辑层面的,而非数据库)。
白话文:把长活事务拆分子事务,提供了正向重试或者逆向回滚
原文地址:https://copyfuture.com/blogs-details/20210129153450331v?ivk_sa=1024320u
假设我们要实现一个转账场景,我们先设想一下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:自己放弃了,让其他服务调用他们自己的逆向接口
逆向回滚要自己用代码去实现,代码复杂的话,疯了。。。
为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行
在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制
分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效
为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
主要要求:
次要要求:
创建一张表,里面方法名称字段为唯一的。想要执行某方法的时候向该表执行插入操作,执行完成之后删除该记录即可。因为方法名称字段唯一,所以在并发的时候只能插入一条记录,其他的并不会执行。当我们要锁住某个方法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
白话文:建一张表,方法名称字段是唯一识别,插入数据成功(代表获取锁成功),插入数据失败(代表获取锁失败),删除数据成功(代表释放锁成功)
这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
除了可以通过增删操作数据表中的记录以外,其实还可以借助数据中自带的锁来实现分布式的锁。
我们还用刚刚创建的那张数据库表。可以通过数据库的排他锁(for update)来实现分布式锁。 基于MySql的InnoDB引擎,可以使用以下方法来实现加锁操作:
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁,当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁,直接catch报错,延迟,进入下次循环
白话文:第一步:关闭事务自动提交;第二步,对查询操作加排他锁(注意:这个表是一个空表。里面可以没有任何数据,查出来为null,只要保证查询不报错就行,只用到了数据库排他锁的这个特性)
白话文:提交事务就可以了
关键字:setex、LUA算法
原文链接:敖丙之如何用Redis实现分布式锁
SETNX key value
SETEX key seconds value
setex的操作是原子性操作,set value和set expire会同时完成
存在的问题:setnx加锁后没有过期时间,一旦服务加锁后,突然出问题了,没有释放锁,会导致锁一直被占用
存在的问题:setnx+set expire是是非原子性操作,可能setnx成功了,但在set expire之前出故障了,还是会有问题
setex是原子性操作,可以避免这个问题
解锁的逻辑更加简单,就是一段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接一下
解决方案:value可以存这个服务的信息,删之前先校验一下是不是自己存的key
白话文:谁加锁必须由它自己解锁,其他线程不能解锁,不然就乱套了
如何使用延迟队列可以参考文章:https://www.jianshu.com/p/6ee386dd166d
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);
尝试去获取锁,和Lock类有点相似,加锁的内部使用LUA语法,具体见原文
主要是判断锁是否存在,不存在就设置过期时间,占用锁;如果锁已经存在了,那对比一下线程,线程是一个那就证明可以重入,锁在了,但是不是当前线程,证明别人还没释放,那就把剩余时间返回,加锁失败。
白话文:尝试去获取锁,如果锁存在,且是当前线程,那么给计数+1(hincrby给数值递增1)
锁的释放主要是publish释放锁的信息,然后做校验,一样会判断是否当前线程,成功就释放锁,还有个hincrby递减的操作,锁的值大于0说明是可重入锁,那就刷新过期时间。
如果值小于0了,那删掉Key释放锁。
白话文:判断是否当前线程,是的话计数-1(hincrby递减1),如果计数值小于0,则释放锁(和AQS的status有点像)
TODO
原文链接:https://blog.csdn.net/qq_21873747/article/details/79485814
比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多。
如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter
个人理解:设定时间T,设定最大容量maxSize,设置计数器counter,来一次请求counter+1,如果在T时间counter数大于maxSize,对接口限流,否则把counter清零,到下个T时间段
通过上图我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在1秒里面,瞬间发送了200个请求。
我们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的系统。
导致这个问题出现的原因是我们统计的精度太低。
滑动窗口算法
在上图中,整个红色的矩形框表示一个时间窗口,在我们的例子中,一个时间窗口就是一分钟。然后我们将时间窗口进行划分,比如图中,我们就将滑动窗口划成了6格,所以每格代表的是10秒钟。每过10秒钟,我们的时间窗口就会往右滑动一格。每一个格子都有自己独立的计数器counter,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。
我们可以看上图,0:59到达的100个请求会落在灰色的格子中,而1:00到达的请求会落在橘黄色的格子中。当时间到达1:00时,我们的窗口会往右移动一格,那么此时时间窗口内的总请求数量一共是200个,超过了限定的100个,所以此时能够检测出来触发了限流。
小tips:计数器算法可以看成只有一格的滑动窗口算法。(然并卵)
当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
TODO
漏桶算法(Leaky Bucket)是网络世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)时经常使用的一种算法,它的主要目的是控制数据注入到网络的速率,平滑网络上的突发流量。漏桶算法提供了一种机制,通过它,突发流量可以被整形以便为网络提供一个稳定的流量。
在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
关键字:流入请求、流出请求
流入请求速度多快无所谓,只要不超出桶的容量就行,流出请求的速度恒定不变
系统性能突然变得特别好,但是流出请求的速度因为之前的缘故设定死了,改不了,只能慢慢来,系统的资源不能得到充分利用
令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。
典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
白话文:令牌生产速度恒定,生产后的令牌会丢到桶里去,如果桶塞满了,就停止生产令牌
传送到令牌桶的数据包需要消耗令牌,不同大小的数据包,消耗的令牌数量不一样。
令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。
白话文:令牌就是一个字节,桶就是存放字节的,有最大容量
如果令牌桶中存在令牌,则允许发送流量。而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。
白话文:1.桶中令牌或者令牌很少(一次请求可能需要消耗多个令牌,不够用),会限流;桶中令牌超多,可以以最快速度处理请求(解决了漏桶算法恒定速度的问题)
主库负责写,从库负责读(待扩充。。。TODO)
垂直拆分是指对数据表列的拆分
把一张列比较多的表拆分为多张表
如用户最近一次登录时间
text类型,如个人介绍
使得列数据变小,在查询时减少锁占用时间,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护
主键会出现冗余,拆完后的两个表主键必须保持一致,这个需要代码去控制
事务处理变得复杂,需要对拆分后的表同时执行事务操作
水平拆分是指数据表行的拆分
保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表(表1、表2、表3.。。。。。)或者库(不同库就可以用同样的表名了)中,达到了分布式的目的
对于一些无穷增长的表(如操作日志表),提升机器性能、增加存储的方式解决不了,这时候就要对表进行水平拆分,拆层多个相同的表
注意:水平拆分建议分库水平拆分,放到不同的服务器上,这样才能有效缓解单台服务器的压力
解决了无穷增长的表的问题
分片事务难以解决 ,跨节点Join性能较差,逻辑复杂
分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。 当当网的Sharding-JDBC 、阿里的TDDL是两种比较常用的实现。
在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360的Atlas、网易的DDB等等都是这种架构的实现。
更多具体的实现方式可以参考文章:
MySQL 对于千万级的大表要怎么优化?
Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零:
特点:旧代码迁移成本几乎为零
<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>
配置库的IP呀、表名呀等等
这个类用来根据entity_key值确定使用的分表名。参考sharding提供的示例代码进行修改。
SingleKeyTableShardingAlgorithm应该就是分配策略
在分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了
个人理解:分布式系统下,在分库分表之后唯一识别ID
原文地址:JavaGuide 分布式 ID 常见解决方案
主要是考虑去哪里获取到这个分布式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
实现起来比较简单、ID 有序递增、存储消耗空间小
批量获取,然后存在在内存里面,需要用到的时候,直接从内存里面拿就可以了
号: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;
相比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小。另外,为了避免单点问题,你可以从使用主从模式来提高可用性。
存在数据库单点问题(使用主从模式来提高可用性)
使用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"
性能不错并且生成的 ID 是有序递增的
存在单点问题,所以要做成Redis集群
比如使用 UUID 作为 MySQL 数据库主键的时候就非常不合适:
生成速度比较快、简单易用
1 个 bit 是不用的 + 用其中的 41 bit 作为毫秒数 + 用 10 bit 作为工作机器 id + 12 bit 作为序列号
需要解决重复 ID 问题(依赖时间,当机器时间不对的情况下,可能导致会产生重复 ID)。
UidGenerator 是百度开源的一款**基于 Snowflake(雪花算法)**的唯一 ID 生成器。
Leaf 提供了 号段模式 和 Snowflake(雪花算法) 这两种模式来生成分布式 ID。
Tinyid 是滴滴开源的一款基于数据库号段模式的唯一 ID 生成器。
分布式链路追踪就是将一次分布式请求还原成调用链路,将一次分布式请求的调用情况集中展示,比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
可以通过调用链结合业务日志快速定位错误信息
各个阶段链路耗时、服务依赖关系可以通过可视化界面展现出来
通过分析链路耗时、服务依赖关系可以得到用户的行为路径,汇总分析应用在很多业务场景
通过事先在日志中埋点,找出相同traceId的日志,再加上parentId和spanId就可以将一条完整的请求调用链串联起来
图片链接:https://blog.csdn.net/dabaoshiwode/article/details/109673681
traceId串联请求形成链路,每一条局部链路都用一个全局唯一的traceId来标识
表示调用自己服务的父服务,从1开始递增
白话文:说服务id感觉也不太对。。。
服务A的方法调用了服务B、服务C,但是不知道调用的先后数据,spanId就是表示这个先后顺序的,从1开始,越小的越先调用
TODO
当查询某条记录时,即让数据库为该记录加锁,锁住记录后别人无法操作
适用原子类比如AtomicLong,内部采用CAS机制实现
Java 8中新增了一个LongAdder类,也是针对Java 7以前的AtomicLong进行的优化,解决的是CAS类操作在高并发场景下,使用乐观锁思路,会导致大量线程长时间重复循环。
LongAdder中也是采用了类似的分段CAS操作,失败则自动迁移到下一个分段进行CAS的思路
在执行所有方法前,先去获取分布式锁,获取成功后再执行后续操作
图片链接:https://www.sohu.com/a/436796808_411876
保证了数据的准确性
适用低并发、无秒杀的场景
图片链接:https://www.sohu.com/a/436796808_411876
参考java里的ConcurrentHashMap的源码和底层原理
实现太复杂了
TODO
TODO
TODO
TODO
短URL从生成到使用分为以下几步.
原文地址:https://blog.csdn.net/yanpenglei/article/details/100788735