00:01
前面我们展示了订单确认页的这些数据,那接下来我们要做的工作就是这个提交订单,那提交订单我们点击以后,那这个订单呢就创建出来,在数据库里边呢,就有一份订单了,那接下来就是进入我们的支付流程,但提交订单这一块有一个非常重要的工作,假设我们模拟现在网速很慢,用户不知道他呢点击了多次提交订单,那这样呢,有可能导致我们数据库里边同一个订单被我们插入了多份,所以呢,我们这一块的功能就是我们订单的防重复提交是一个非常重要的,那么用专业的术语就是我们要保证提交订单的密等性,那这个密等性。他呢解释就是我们提交一次跟提交100次结果都是一样的,数据库只会有这一份订单,所以我们接下来就来完整的讨论一下我们密等性相关的问题,大家来打开我们给大家的相关文档,来到这个文档里边课件,我们第二天呢,来讨论一下我们接口一等性,在我们这个分布式系统里边,无论是我们给页面提交数据,我们表单的提交,还是呢,可能分布式系统AB系统之间的互相调用,那有可能呢,同一个方法我们都会执行多次,比如用户点击了多次提交,或者呢,我们这个调用一次不成,我们多次调用,那我们。
01:31
无论是怎么调用,我们一定要保证我们这个接口的密等性,那什么是密等性,密等性呢?就是说我们接口密等性就是用户对于同一操作的发起,无论是发起了一次请求还是多次请求,他们的最终结果都是一致的,也就是说我们提交订单,无论用户在这点了十次还是点了一次,那最终呢,只有一个订单,就是数学里边的密等,我们给一个数字求上无论多少次幂,比如一的一次幂和它的100次幂,它的结果呢都是一,所以我们现在就要保证我们这个幂等,那这个幂等呢,特别对于我们一些关键场景,比如我们支付,我们来到了这个支付确认页,我们在这儿输入支付密码来点击支付,那支付呢就应该扣款成功,当然用户网速很慢,把支付点了多次,那同一个订单会不会多次扣款呢?所以这一块场景我们一定要考虑。我们的密等就是无。
02:31
问,我们发起一次操作请求还是多次操作请求,最终呢,他只做了一件事情,而且这件事情也只被成功的做了一次,所以我们现在就要保证我们接口的密等,但是哪些情况我们要防止我们这些密等性,也就是防止重复提交,比如我们在页面上用户的多次按钮点击,这是我们第一个我们要方式,还有我们页面回退再提交,比如我们这个成功页已经成功了,我们退回去再提交,还是提交了这个订单支付,所以我们这些呢,可能也要防止,以及我们微服务之间互相调用,比如A服务和B服务,我们A服务呢,假设是下订单,我们要调B的减库存。
03:14
但是呢,B的减库存第一次没调成功,我们重试了一次,又调了一次减库存,但我们也不知道B的第一次调用是由于他的这个库存没减成功,还是网络问题,人家减成功了,给我们返没返回数据,所以如果我们再来调用第二次,如果上一次是因为减成功了,返回数据有问题了,那我们再调一次是不是又减了库存,所以这呢也是一个很大的问题,那么这些呢,都要防止我们这些接口的多次提交,也就是我们要保证它的密等性,以及我们其他各种业务情况,我们需要的一些特殊情况,我们都要保证这些密等性,而且我们来讨论一下什么情况需要密等,比如以我们数据库为例,我们circleq为例,我们好多操作呢,都是要给数据库增删,检查一些数据,那么呢,有些操作是我们说的天然密等呢,比如我们举一个例子,查询数据来查询这个一号订单,那这个订单你第一次发请求可能远程服务有问题或者怎么着。
04:15
好,我们重试一次,再发一号订单的查询请求,只要这个订单在这儿,我们无论查询多少次,这个结果呢,都是一样的,不会引起我们数据库的一些变化和我们业务上的一些变化,所以呢,我们这个查询它是我们天然蜜等的,包括我们的这个更新,我们这个固定更新,我们把一列的值固定改成一个值,比如我们一个东西的状态,我们给它改成一或者零,我们这个更新的circle,我无论发生多少次,我方法的第一次调用,第二次调用,第三次调用,我无论发生多少次请求,那最终在数据库,那么这个呢,都是一样的,第一次把它改为一,第二次呢还改为一,其实呢,永远都是,那这个呢,也是一个密等操作,包括我们这个删除,比如我们来删除一号用户,那这个一号用户删除,我们无论发送多少次请求,比如在页面点了删除删除删删除,无论点多少次,我们这个用户呢,因为只存在一个,其他人呢,也不会有用户ID试机。
05:15
所以我们这个只要第一次删除成功以后,删除呢,其实都是没有删除数据的,所以他们呢也是密等的,无论发多少次请求都是结果一样的,而我们这个插入,插入呢,比如我们是按照主键插入,就是我们插入的时候,我们来指定了主键,这个也是幂等的,来举一个例子来插入一号用户,我们这个主键呢,不让他自增,我们每次插入呢,给他指定主键,那一号用户张三,我们把它呢执行,不管多少次,第一次插插入到了你这个一号用户,我在第二次再来执行,因为一号已经有了,而且主键是唯一的,不重复的,你想要再插入数据库都报错了,所以我们的带主键的插入这呢也是密等的,但是我们下边的这些操作就不是密等的,比如我们更新一个数据,这个数据呢,是一个叠加状态的更新,比如我们来更新库存,库存呢,我们每次给它减一,减一我。
06:15
我们第一次发了一个减一请求,那库存呢就减一个,如果我们有了一些重试机制,或者我们在这儿呢,不断减了库存,那就导致我们这个库存执行一次跟执行多次肯定结果是不一样的,所以我们这个呢就不是密等的,那么同一个订单减库存一定呢,只能执行上一次。包括我们的这个插入,比如我们这个插入呢,我们不是用主键,像我们以前的这个主键都是自增的,我们就插入上一条数据,比如按照我们的这一块设计,我们这里边呢,有一个订单来打开,订单呢,它的ID是自增的,然后假设我们订单号我们没有限制,唯一我们每次插入订单,那接下来一号订单我们往进一插,然后呢,它第一条记录ID就是一,一号订单呢,我还可以再插入进来,它还是二,所以我们这个如果不带主键的这个插入,那呢,它也不是幂等的,我们可以多次插入,所以呢,我们这一块的这些情况呢,都不算是密等的,但如果我们这个订单订单号呢,我们数据库做了唯一约束,比如我们这个订单号为123的这个订单,我们的这个请求,我们点了一个创建订单,提交订单,然后呢,用户点了多次,那都要提交这个123,这个订单第一次给数据库里边插了一个123。
07:37
订单以后想要再来插入我们这个123订单,因为我们数据库如果做了唯一约束,那这个订单呢,就不能再插入了,所以我们在订单这一块,我们的订单号我们可以在这儿改变表,我们一定要把这个订单号这一块,我们来给它做成一个唯一约束,我们可以来给它添加索引,来指定我们的订单号呢,一定是唯一的,我们这个订单里边只能有。
08:03
同样的订单号只能有一个,我们来点一个保存。好,这呢就相当于我们在数据库设计级别,我们就可以保证我们同一个订单只有一条记录,这是我们以数据库为例,我们哪些操作是密等的,哪些不是密等的,那其他的以我们这个业务为例,大家也应该去考虑我们这个业务,如果我们点了这个按钮多次,把业务的流程我运行了两次,那这两次的结果,如果第二次结果跟第一次结果永远呢都是一样的,那我们就是密等的,否则呢就是不密等的。所以我们现在呢,在一些关键核心操作,你一定要保证我们的密等,那如何来保证我们的密等,就我们无论做多少次这个提交订单,那么当前这个页面的提交订单肯定只能提交成功一次,那怎么做这件事情呢?我们说密等性的解决,我们可以有很多种方案,比如我这儿列举了常见的五种方案,第一种是token机制,Token呢,就是我们说的令牌机制。
09:07
这个令牌机制大家最常见的那就是验证码了,比如大家去12306去买票,如果我们来选中了座位,我们要锁定座位来点一个提交,12306呢,要我们必须来输入一个验证码,那只有我们带上了这次验证码,验证码是对的,我们这次请求呢才能提交过去,那验证码是错的,或者呢,我们拿着第一次的验证码,我们再来提交第二次的请求,那肯定就是有问题的,所以我们的这个令牌机制我们就可以来使用它。令牌机制的工作原理就是比如我们来到这个页面,我们想要提交订单,我们给页面上的服务端,给页面放上一个令牌,这个令牌假设就叫123456,然后我们来点提交订单,就会带上令,这个令牌服务器呢,提前存储了这个令牌,叫123456,然后我们提交订单的请求,带的这个令牌跟服务器的令牌一模一样,那服务器呢,就算验证通过了,给我们执行创建订单。
10:07
啊,而且呢,只要我们这个令牌验证通过,我们服务器就把这个令牌删除了,那接下来如果是模拟用户的多次提交,用户第一次点提交订单,他带的令牌是123456,因为他重复不断的点这个按钮,他带的这个令牌呢,还是123456,所以呢,我们这五次的123456,只有第一次能匹配成功,剩下的四次就是失败的,所以我们呢就能保证密的,所以我们可以来使用这个令牌机制,令牌机制呢也可以是验证码来做,我们提交订单的时候,让用户自己在这儿输一个验证码。验证码呢,服务器存一个用户页面输一个,然后呢,用户页面输的是12345,服务器当时给他生成的这个验证码也叫12345,然后用户只要去提交,包括他多次提交,他提交的都是相同验证码,我们就呢把它以后的只有第一次放行通过以后就打回去了,所以我们的令牌机制接下来在一定程度来保证我们的密度,但这个令牌机制呢,也有一定的危险性,比如我们来举一个例子。
11:11
我们这个令牌,由于页面我们现在呢要提交一个令牌,比如是123456,然后呢,服务端有一个令牌123456,那么现在要做的事情就是令牌,我们要怎么验证令牌才是一个比较完美的操作,不会出现问题,是先删令牌还是业务执行成功以后后删令牌?首先我们来想一想,如果是我们后删令牌,就是我们点了提交订单,然后呢,订单创建完了,把这个令牌删掉,我们第二次再点提交订单,带着相同的令牌,我们肯定呢就不行了,如果是后山令牌有没有问题,那么后山令牌问题很大,假设呢,我们这个提交订单一23456,我们这个令牌呢,就叫123456,我们用户点了多次,咔咔两次连着点,那第一个请求呢,123456带进来,由于我们是后山令牌,所以呢,我们第一个请求在这儿正在执行创创建订单,第二个请求就进来了。
12:11
然后呢,带着123456,由于这个令牌呢还没删,所以他还能用它呢,也在这儿创建订单,所以呢,我们可能至少呢,就有两个我们能同时创建的,所以我们必须先删令牌,但先删令牌呢,又会有一些问题,比如我们删令牌的伪代码操作,我们是这样写的,我们一般呢,在服务器里边我们来这么用,我们浏览器端带了一个令牌,比如我们前端带来的令牌,我们就叫code,我们前端带来的叫ton ton呢等于123,我们是controller,收到请求以后,我们就收到了这个token,好比如我们这个controller,我们controller呢,收请求的时候就收到了这个token,那收到这个token以后呢,接下来我们就有一个判断,If,如果说我们服务端保的这个token,如果我们服务端,我们server保存的这个token,然后呢,跟你提交来的token等等,我们提交来的这个token是一样的,那是一样的呢,接下来我们就要执行业务逻辑和。
13:11
删令牌,所以接下来我们要做的事情就是先删令牌,Delete token,我们令牌删了以后,我们再来执行业务逻辑,我们的service方法调用,那业务逻辑执行完了以后,假设我们这个请求是重复提交两次进来,咔咔两次连续进来,那这样我们第一次令牌我们对比成功,已经删掉了,我们再执行业务逻辑了,第二次进来,我们再来判断服务器的token已经跟这个不一样了,所以呢我们这个就可以保证,但是呢,我们说这种有一定的危险性,因为我们这个令牌在分布式系统下,我们一般呢都存在red里边,我们不可能每一个项目存在他自己的内存里边,这样肯定有问题,所以呢,我们会存在red里边,所以我们拿从服务端拿token,我们还是要从red里边拿,我们来拿来我们当时保存的这个token,拿来以后呢,再对比再删,那假设用户的这两次请求很快,咔咔两次请求进来,然后。
14:11
那现在两次请求同时去red里边都拿到这个令牌,然后呢,同时都对比成功,同时进来删令牌,同时执行了业务逻辑,所以呢,这就会出现一定的风险,所以为了保证这个风险,我们使用令牌机制,我们一定要保证,保证什么呢?我们获取令牌,我们第一个呢,是从服务端从red里边获取令牌,Get token,我们获取令牌,获取来的令牌要跟我们前端带来的这个令牌我们对比,然后呢,对比成功,我们再来删令这三个操作。获取第一个操作是获取,第二个操作是对比,第三个是山一定要是一个原子性的,他们是一个不可分割的,不能说诶我在这儿拿令牌了,令牌拿到呢,我在这儿判断期间另一个请求进来,也跟我一样,人家还执行的快,也令牌拿来了,我们两个呢,还同时对比成功了,同时删令牌了,那就没防住,所以我们这个令牌这一块,这三个操作要保证原子性才行,那这个原子性其实我们以前用red,特别我们再来讲分布式锁的时候,我们分布式锁的原理就是我们的占坑与设置过期时间,我们是整个一个原子操作,在red里边要用原子操作,我们就可以用我们的lua脚本,所以我们最终呢,如果我们要用令牌,我们在red中,我们对比令牌,整个操作应该是这样子的,我们使用一个撸R脚本。
15:47
我们调用red的call,然后这个脚本呢,是从red里边获取我们指定的这个令牌数据,如果等于前端,这个应该是前端传来的令牌,如果两个对比相等,把这个令牌就删掉,否则呢反馈零,所以呢,我们用一个脚本来保证令牌的完整性,这门说密等性,如果我们使用令牌机制,我们就这么来做,这个令牌可以是一个验证码,也可以是服务端随便生成的一个令牌,在页面里边放好都行。那接下来我们说还有我们的各种所集制,特别是对于我们数据库来说,其实我们说了万物皆是增删改查,我们只是增删改查的地方不一样,我们有有些地方是给马S库,我们数据库里边做增删改查,有些呢是给red里边做增删改查,有些呢,可能是给我们一些云服务器里边做一些增删改查,所以假设我们这个创建订单,我们呢是一个明显的增删改查操作,创建订单就是要给数据库里边插入一条订。
16:48
啊,所以我们应该做的是请,就是我们可以使用各种所机制,为了保证我们的这个同一时间,也不说同一时间了,就是第一次请求,第二次请求进来,我们同一个东西呢,只会被执行一次,那么这个密等的最大特点就是我们用户咔咔两次直接点击或者我们的份进行远程重试,第一秒试了不成功,第二秒又试,第三秒又试,那么短时间内呢,我们同一个请求执行多次,这个时候呢。
17:17
如果我们这个是在数据库级别的。我们要做数据库的一些操作,我们就可以给数据库加锁,那数据库常见的就两种锁,悲观锁,乐观锁,那悲观锁比如我们这个查询,特别是比如我们这个查询库存,库存我们就可以使用我们的select,带一个for update,那么查询的时候呢,同时就锁定了这条记录,这条记录呢就不会被别人动态,那这样我们第二个请求进来,我们相当于就得等待,我们可以使用悲观锁,当然我们如果一些更新场景,我们也可以来使用乐观锁,就是数据库的各种锁机制,看我们哪些业务,哪种场景合适,我们去来用各种锁机制,数据库的悲观锁机制,我们select for update的还有乐观锁机制,特别我们这个更新,比如我们这个更新数据,我们可以给他带上版本号的更新,我们以前讲过这个勒索关锁机制,如果我们两次请求同样是重复提交进来的,那么第一次更新只要我们更新成功了,它的版本号就会叠加,第二次呢?
18:20
再来更新,有两次请求呢,是重复提交的,他还带着以前的老版本号想要更新,但是我们的版本号都已经叠加上来了,所以呢,他的第二次更新可能会失败,所以呢我们就可以使用它的乐观锁机制,但到底使用哪种锁,我要视我们的业务场景而定,比如我们这一块呢,说了一个场景,比如我们在操作库存之前,我们可以先来获取我们商品的这个版本号,然后呢,我们操作库存的时候带着版本号,如果我们这个库存操作成功,我们就给版本号加一,那然后呢,别人在想要操作这个商品的库存,而且我们是一个重复请求,他第二次重试的时候呢,最大的特点就是他还带着以前的老数据,比如他还带着我们商品这次查库存。
19:07
或者锁定库存的时候,版本号呢还是一,然后呢,我们一看版本号是一,想要在这儿更新,诶我们这个版本已呢,已经变成二了,这一次更新就不成功,所以我们也可以来使用这个乐观锁,包括我们都可以来使用我们的分布式锁,分布式锁呢,比如我们这种场景,我们现在上线了好几台机器A。BC,那么这三台机器呢,假设都是订单服务,哎,假设我们都是库存服务吧,库存服务假设呢,我们现在订单失败了,我们库存呢都要解锁,而且我们这个解锁呢,假设我们是一个定时任务,我们呢有一个定时器,我们每隔一段时间从我们数据库里边扫描这些失败了的订单,然后呢,把失代败的订单我们都来解锁,但这三排我们的库存服务。服务器呢,同时我们这个定时任务触发了,一触发以后呢,他们同时拿到了一号二号一号二号一号二号这两个失败的订单,他们又同时来进行解锁。
20:10
所以这种情况下呢,我们就可以使用分布式锁,比如我要解锁一号订单,我A机器要解锁哪个订单,我给它加一个分布式锁,别的机器呢,想要解锁这个订单,那他呢,由于获取不到分布式锁,他就必须等待,然后我们这个解锁成功了,他获取到分布式锁了,他一看我们这个订单呢,还有一个标志位,它已经被解锁过了,那它呢也就不解锁了。所我们也可以结合我们业务的分布式锁,包括我们时说的各种唯一约束,为了防止我们同一个东西被执行多次,特别我们这个订单插入这个同一个订单呢,创建了多次,那怎么办呢?我们接下来可以做的事情,那就是我们让这个订单给数据库插的时候,订单号是唯一的,诶,订单号是唯一的,这样呢,你重复的多次请求,假设我们提交订单的时候带了一个订单号,咱提交订单带订单号有点不合理,我们假设啊,我们现在带了一个订单号,我点了两次提交,第一次呢,我们插入数据库,插入成功了,然后。
21:10
那么第二次提交,由于订单号是唯一的,我们还想要带着原订单号,那给数据库里边查记录,我们发现呢,这个记录就是失败的,失败的原因就在于我们数据库里边有唯一约束,你想把同样的东西查两遍是不可能的,所以呢,我们就可以数据库的唯一约束,包括我们red里边的一些防虫处理,比如哪个数据我们处理过了,我们就不处理了,特别我们说的以前大家用过百度网盘的这个秒传功能,这秒传功能呢,其实就是这样,它的这个防虫防的也有点多,比如我们以前上传过的这个一个文件,只要我们这个文件上传过了,我们这个文件呢,给他计算一个MD5值,因为所有东西的MD5值都是唯一的,这样呢,你还想再次处理这个文件,还想再次上传百度一看,诶,我们这个MD5已经存在了。
22:03
那么就不用上传了,你用以前的数据,我直接把以前的数据给你返回,你拿着用都行,所以我们现在呢,就是这样,我们也可以用这些各种防虫功能们哪个数据只要被处理一次,我们放,比如放到red的集合里边,这个数据再提交过来进行处理,我们一看集合里边已经存在了这个数据处理过的记录,我们就不需要处理了,所以呢,我们也可以用这些防虫,当然防虫呢,不只可以放到red里边,我们也可以给数据库里边放一些防虫表,特别是订单这些处理,只要我们这个订单,比如我们这个解锁库存。这个操作我们只要处理过了,我们就给防虫表里边插一条记录,订单解锁库存已经操作过,然后呢,你再想要调这个订单解锁库存,因为你要解锁库存呢,你就得给防虫表里边先插一条订单解锁库存的记录,你能查成功,我才给你解锁,结果呢,你一插入插入失败了,所以呢这就有问题,所以我们可以做各种的防冲处理,无论我们是去reds里边建防虫表,我们的数据库里边建防虫表,我们都可以来做一个防虫处理,包括我们全局的唯一请求ID,每一个请求过来,那么都上带上一个唯一ID,你的这个请求只要被执行多次,那就不行,特别是我们这个粪,由于我们这个粪要进行重试,我们A系统调用,B系统这个远程调用,那么A调用,B调用失败了,他要进行重试,重试最大的特点就是拿着以前的老请求再发一遍,所以呢,我们可以为每一个请求制定一个ID。这样的话呢,我。
23:40
我们再发过去,发到BB一看,你的这个请求我已经处理过了,那我就直接给你返回成功,这样呢我们就可以来做这个事情,包括呢,如果我们全局请求的唯一ID是从页面开始转到恩恩,直接给他设一个唯一ID,我们还可以做后来的链路追踪,我们来看一下这个请求都经过了哪一些流程,它被转到了A系统,又转到了B系统,又转到了C系统,那还可以做一个链路跟踪,比如我们可以在NG里边,我们在那里边再设置一个header,我们以前呢,设置保存过一个header,那么现在呢,重新再来设置每一个请求,只要过NN都给它分配一个唯一ID,当然如果是N层面分配了唯I唯一ID,嗯,没办法去来做我们的防虫处理,因为我们提交的这两个请求一二,我们每一个请求呢,第一个请求过了NG4,第二个请求也过了NG4 NG4给每一个请求都给了一个唯一ID。
24:39
所以这两个呢,相当于不一样NT不区分这两个请求是什么样的,但是我们这个唯一ID呢,如果是我们的份来进行AB2个系统的调用,那就非常实用了,所以我们这就是我们说的密等性处理的各种场景,当然这些呢,大家去来理解一下,我们可以用验证码令牌机制,也可以用各种的锁机制,数据库的乐观被关锁,我们业务的一些分布式锁,加上以后就可以解决问题,包括我们也可以做好数据库的各种约束,做好防虫表。
25:12
再带上每一个请求的各种唯一ID等等,这呢都是可以来解决我们这个密等问题的,那我们下一课呢,就来解决我们这个订单提交的这个密问题。
我来说两句