架构师成长之路 - 幂等性是什么?如何设计幂等性系统?

Author: Xcourse   2023-Mar-22 23:02   Reads: 851

欢迎加入微信工作内部分享群,每天发布新的精选高薪工作。

官方邮箱:enquiry@xcourse.sg

微信分享群:@新加坡工作内部分享群

WhatsApp群:@Singapore Jobs & Internships

Telegram中文群:@新加坡工作内部分享群

Telegram英文群:@Singapore Jobs

------------------------------------------------------------------------------------------------------

一. 幂等性是什么?

幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。

 

二. 接口为什么要实现幂等?

前端重复提交选中的数据,后台只产生对应这个数据的一个反应结果。

 

三. 幂等常用思路

(1)token机制     

当客户端请求页面时,服务器会生成一个随机数token,并且将token放置到session当中,然后将token发给客户端(一般通过构造hidden表单)。下次客户端提交请求时,token会随着表单一起提交到服务器端。服务器端第一次验证相同过后,会将session中的token值更新下,若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的token没变,但服务器端session中token已经改变了。

(2)乐观锁

(3)通过版本号实现

update table_xxx set name=#name#,version=version+1 where version=#version#;通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= ,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;

(4)去重表

利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引。需求是博客点赞问题,要想防止一个人重复点赞,可以设计一张表,将博客id与用户id绑定建立唯一索引,每当用户点赞时就往表中写入一条数据,这样重复点赞的数据就无法写入。 

  • 我们可以借鉴数据库的乐观锁机制来举个例子:

1、首先为表添加一个版本字段version

2、在执行更新操作前呢,会先去数据库查询这个version

3、然后执行更新语句,以version作为条件,例如:
UPDATE T_REPS SET COUNT = COUNT -1,VERSION = VERSION + 1 WHERE VERSION = 1

4、如果执行更新时有其他人先更新了这张表的数据,那么这个条件就不生效了,也就不会执行操作了,通过这种乐观锁的机制来保障幂等性。

 

四. 什么情况下会出现重复消费?

当消费者消费完消息时,在给生产端返回ack时由于网络中断,导致生产端未收到确认信息,该条消息会重新发送并被消费者消费,但实际上该消费者已成功消费了该条消息,这就是重复消费问题。

 

五. 如何避免消息的重复消费问题?

消费端实现幂等性,就意味着,我们的消息永远不会消费多次,即使我们收到了多条一样的消息

业界主流的幂等性操作:

  • 唯一ID + 指纹码机制,利用数据库主键去重
  • 利用Redis的原子性去实现

(1)唯一ID+指纹码机制

  • 唯一ID + 指纹码机制,利用数据库主键去重
  • SELECT COUNT(1) FROM T_ORDER WHERE ID = 唯一ID +指纹码
  • 好处:实现简单
  • 坏处:高并发下有数据库写入的性能瓶颈
  • 解决方案:跟进ID进行分库分表进行算法路由

整个思路就是首先我们需要根据消息生成一个全局唯一的ID,然后还需要加上一个指纹码。这个指纹码它并不一定是系统去生成的,而是一些外部的规则或者内部的业务规则去拼接,它的目的就是为了保障这次操作是绝对唯一的。

将ID + 指纹码拼接好的值作为数据库主键,就可以进行去重了。即在消费消息前呢,先去数据库查询这条消息的指纹码标识是否存在,没有就执行insert操作,如果有就代表已经被消费了,就不需要管了。

对于高并发下的数据库性能瓶颈,可以跟进ID进行分库分表策略,采用一些路由算法去进行分压分流。应该保证ID通过这种算法,消息即使投递多次都落到同一个数据库分片上,这样就由单台数据库幂等变成多库的幂等。

 

(2)利用Redis的原子性去实现

我们都知道redis是单线程的,并且性能也非常好,提供了很多原子性的命令。比如可以使用 setnx 命令。

在接收到消息后将消息ID作为key执行 setnx 命令,如果执行成功就表示没有处理过这条消息,可以进行消费了,执行失败表示消息已经被消费了。

使用 redis 的原子性去实现主要需要考虑两个点

  • 第一:我们是否要进行数据落库,如果落库的话,关键解决的问题是数据库和缓存如何做到原子性?
  • 第二:如果不进行落库,那么都存储到缓存中,如何设置定时同步的策略(同步到关系型数据库)?缓存又如何做到数据可靠性保障呢

关于不落库,定时同步的策略,目前主流方案有两种,第一种为双缓存模式,异步写入到缓存中,也可以异步写到数据库,但是最终会有一个回调函数检查,这样能保障最终一致性,不能保证100%的实时性。第二种是定时同步,比如databus同步。

  1. 使用redis的setnx命令的情况下,如果消费者端setnx成功后,进行消息消费,但是此时突然宕机。那么对于接下来一段时间内(指代锁的有效时长),就无法保证消息的及时消费?

答:首先宕机问题要尽量避免,通过一些高可用的方案降低宕机的风险,如果确实宕机了,对于已发送但未被消费的消息,可以自己去做补偿或者投递到延迟队列里处理,宕机会造成生产端消息堆积,如果对消息实时处理要求比较高,需要提前预备一些应急方案另起服务去处理这些消息。

  1. redis的setnx怎么做幂等性的? 锁的有效时长设为多少呢

redis实现幂等很简单,我以redis实现接口的幂等性为例说明。你可以自定义一个幂等注解,然后配合AOP进行方法拦截,对拦截的请求信息(包括方法名+参数名+参数值)根据固定的规则去生成一个key,然后调用redis的setnx方法,如果返回ok,则正常调用方法,否则就是重复调用了。这样可以保证重复请求接口在一定时间内只会被成功处理一次。至于锁的有效时长要根据业务情况而定的。

 

六. mq如何保证消息的幂等性

一、出现非幂等性的情况

1、生产者已把消息发送到mq,在mq给生产者返回ack的时候网络中断,故生产者未收到确定信息,生产者认为消息未发送成功,但实际情况是,mq已成功接收到了消息,在网络重连后,生产者会重新发送刚才的消息,造成mq接收了重复的消息

2、消费者在消费mq中的消息时,mq已把消息发送给消费者,消费者在给mq返回ack时网络中断,故mq未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息;

 

二、解决办法

1、mq接收生产者传来的消息:

mq内部会为每条消息生成一个全局唯一、与业务无关的消息id,当mq接收到消息时,会先根据该id判断消息是否重复发送,mq再决定是否接收该消息。

2、消费者消费mq中的消息:

也可利用mq的该id来判断,或者可按自己的规则生成一个全局唯一id,每次消费消息时用该id先判断该消息是否已消费过


Tags: interview backend architect

Topics: 面经