【学习笔记】:分布式系统中业务的幂等性处理

一、什么是幂等性

幂等是一个数学与计算机学概念。数学中,幂等函数,是指可以使用相同参数重复执行,并能获得相同结果的函数 f(x)=f(f(x))。计算机编程中,幂等操作是指对于同一个操作,用户多次执行所产生的结果均与第一次执行的结果相同,也即同一操作的多次执行并不会对输出结果产生改变。业务中有很多天然幂等的逻辑,如:数据库select查询操作,根据唯一主键进行update、delete操作等。

二、需要做幂等性处理的业务场景

2.1、表单的重复提交

用户订单信息、贷款业务申请信息等表单创建完成后,点击提交,可能会因网络波动没有及时对用户做出提交成功的响应,致使用户认为没有成功提交,然后重复点击提交按钮,这时就会发生表单信息的重复提交。

2.2、接口的重复请求

和表单重复提交相似,如订单支付功能、余额转账功能等业务,可能因为后端涉及的逻辑较多,导致业务执行时间比较长,然后用户看不到成功结果误以为支付失败,页面回退后重新支付或转账,出现连续重复支付的情况。

2.3、消息的重复消费

当使用RabbitMQ等消息中间件时候,如果因为网络原因导致消费者接收消息后没有回执中间件,或者中间件没有接收到消费者发送的回执信息,就可能会导致消息的重复消费。

三、幂等性处理解决方案

3.1、前端防重复提交处理

在表单提交或者订单支付等操作点击确认后,设置按钮为不可点击状态,并在页面显示一个正在处理的加载动画,当业务执行完成后,重定向到操作成功界面,若业务执行失败,提示错误原因,并初始化表单数据。

3.2、防重唯一令牌机制

3.2.1、简要描述

以下单功能为例:

①、在商品界面点击“立即购买”或者在购物车界面点击“立即结算”,这时会调用接口,根据用户选择的商品信息,生成订单信息。在生成订单信息的过程中,我们可以在后端代码中,生成一个UUID作为当前订单的唯一令牌一同返回至前端,并将该UUID以用户id或订单编号id为key保存到redis缓存中。

②、前端收到订单令牌后,将它写入表单的隐藏字段中,在用户确认订单无误后,点击提交,后端会收到提交请求中携带的令牌信息。

③、将请求中的令牌信息和缓存中的获取到令牌信息进行比较,如果一致,代表该请求为首次请求,删除缓存中的令牌后,继续执行后续的业务代码。

④、业务执行完成后,给前端返回操作成功的响应。业务流程执行完毕。

3.2.2、流程图

浏览器 订单系统 Redis 1、选择商品进行下单 2、 根据所选商品信息生成订单 生成当前订单的唯一令牌 3、将令牌以用户id或订单id为key,保存到Redis中 4、将订单信息和令牌响应给浏览器 5、展示订单信息,用户确认无误后,提交订单 6、浏览器将订单关键信息和令牌一同提交至服务器 7、订单系统获取请求中的令牌,根据保存令牌时指定的key去访问Redis 8、Redis尝试获取令牌并进行比对 8.1、若缓存中获取令牌为空,表示当前请求为重复提交 8.2、若缓存中的令牌与请求中的令牌不匹配,表示提交的订单信息已失效 8.2、若缓存中的令牌与请求中的令牌匹配,表示当前请求为首次请求,删除缓存中的令牌 9、令牌比对完成后,根据比对结果,响应订单系统 10、 根据Redis的响应结果, 如果匹配成功,继续执行提交逻辑, 如果匹配失败,响应浏览器订单提交失败。 浏览器 订单系统 Redis

需要注意的几点
1、业务逻辑执行前删除 token 还是后删除 token?
(1) 、先删除可能导致,比对成功后删除了token,但是业务由于某种原因没执行完,重试过程仍带上之前的token,因为防重设计导致后续比对不通过,提交失败。
(2)、 后删除可能导致,业务处理成功后,由于某种原因服务闪断,没有删除token,并且浏览器响应超时,用户继续重试,导致提交业务被重复执行多遍。
(3)、 我们最好设计为先删除 token,如果业务调用失败,就重新获取token再次请求。
2、Token获取、比较和删除必须是原子性
在上述流程中第8步令牌校验的过程中,令牌的获取、比对和删除三个步骤是在同一步操作中完成的,也即是要保存这三个步骤整体的原子性。如果分开操作,在并发环境下,会存在并发安全问题,所以代码中使用脚本来完成整个令牌校验过程:“if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end”

3.2.3、代码

/**
 * 生成订单页
 */
@Override
public OrderConfirmVo confirmOrder() {
    OrderConfirmVo confirmVo = new OrderConfirmVo();
    // 执行订单逻辑
    ......
    // 生成防重令牌
    String orderToken = UUID.randomUUID().toString().replace("-", "");
    confirmVo.setOrderToken(orderToken);
    String key = OrderConstant.USER_ORDER_TOKEN + memberVo.getId();
    redisTemplate.opsForValue().set(key, orderToken, 30, TimeUnit.MINUTES);
    return confirmVo;
}


/**
 * 订单提交
 */
@Transactional
@Override
public SubmitOrderRespVo submitOrder(OrderSubmitVo vo) {
    SubmitOrderRespVo respVo = new SubmitOrderRespVo();
    MemberVo memberVo = LoginUserInterceptor.loginUser.get();
    submitVoThreadLocal.set(vo);
    // 验证防重令牌(原子性)
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class),
                                        Arrays.asList(OrderConstant.USER_ORDER_TOKEN + memberVo.getId()),
                                        vo.getOrderToken());
    if (result != 0l) {
        // 校验通过,保存订单信息
        ......
    } else {
        // 校验不通过,返回失败状态码
        respVo.setCode(OrderConstant.ORDER_TOKEN_CHECK_FAIL);
    }
    return respVo;
}

3.3、数据库层面实现幂等性

3.3.1、数据库唯一约束

在数据库执行insert操作时,可以通过指定唯一索引进行插入,比如订单号、业务流水号等。不能有两条订单号相同的订单同时保存到数据库。
使用数据库唯一约束实现幂等性处理时需要注意的是,该主键一般来说并不是使用数据库中自增主键,因为分布式或者大数据的场景下,可能需要进行分库分表操作,不同的数据库和表主键不相关。
此时可以考虑使用 UUID 或者雪花算法等方式生成一个唯一的编号作为订单号,这样才能保证在分布式环境下 ID 的全局唯一性。

3.3.2、数据库悲观锁

select * from xxxx where id = 1 for update;
悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,需要根据实际情况选用。
另外要注意的是,id 字段一定是主键或者唯一索引,不然可能造成锁表的结果,处理起来会
非常麻烦。

3.3.3、数据库乐观锁

这种方法适合在更新的场景中:
update t_goods set count = count -1 , version = version + 1 where good_id=2 and version = 1
根据 version 版本,也就是在操作库存前先获取当前商品的 version 版本号,然后操作的时候
带上此 version 号。我们梳理下,我们第一次操作库存时,得到 version 为 1,调用库存服务
version 变成了 2;但返回给订单服务出现了问题,订单服务又一次发起调用库存服务,当订
单服务传如的 version 还是 1,再执行上面的 sql 语句时,就不会执行;因为 version 已经变
为 2 了,where 条件就不成立。这样就保证了不管调用几次,只会真正的处理一次。
乐观锁主要使用于处理读多写少的问题

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
当我们使用 RabbitMQ 作为消息队列时,消费者需要处理重复消费的情况。实现幂等性处理可以避免重复消费带来的问题。以下是一个示例,演示如何使用 Redis 实现 RabbitMQ 消费者的幂等性处理: 1. 在消费者应用引入 Redis 的依赖。 2. 在消费者处理消息前,先查询 Redis 是否已经处理过该消息。如果已经处理过,则不再处理;如果未处理,则处理消息,并将消息处理的标识存储到 Redis 。 示例代码: ```python import redis import pika # Redis 配置 redis_host = 'localhost' redis_port = 6379 redis_db = 0 redis_key_prefix = 'rabbitmq_msg_processed:' # 连接 Redis redis_conn = redis.Redis(host=redis_host, port=redis_port, db=redis_db) # RabbitMQ 配置 rabbitmq_host = 'localhost' rabbitmq_port = 5672 rabbitmq_user = 'guest' rabbitmq_password = 'guest' rabbitmq_exchange = 'example_exchange' rabbitmq_queue = 'example_queue' rabbitmq_routing_key = 'example_routing_key' # 连接 RabbitMQ credentials = pika.PlainCredentials(rabbitmq_user, rabbitmq_password) connection = pika.BlockingConnection(pika.ConnectionParameters(host=rabbitmq_host, port=rabbitmq_port, credentials=credentials)) channel = connection.channel() # 声明 exchange 和 queue channel.exchange_declare(exchange=rabbitmq_exchange, exchange_type='direct', durable=True) channel.queue_declare(queue=rabbitmq_queue, durable=True) channel.queue_bind(queue=rabbitmq_queue, exchange=rabbitmq_exchange, routing_key=rabbitmq_routing_key) # 定义消息处理函数 def callback(ch, method, properties, body): msg_id = properties.message_id redis_key = redis_key_prefix + msg_id # 查询 Redis 是否已经处理过该消息 if not redis_conn.exists(redis_key): # 处理消息 print('Received message: %r' % body) # 将消息处理的标识存储到 Redis redis_conn.set(redis_key, 1) print('Processed message: %r' % body) else: print('Message already processed: %r' % body) ch.basic_ack(delivery_tag=method.delivery_tag) # 消费消息 channel.basic_qos(prefetch_count=1) channel.basic_consume(queue=rabbitmq_queue, on_message_callback=callback) print('Waiting for messages...') channel.start_consuming() ``` 在上述示例,我们定义了一个 Redis 的连接对象 `redis_conn`,并在消息处理函数 `callback` 使用该对象查询并存储消息处理的标识。如果消息处理过,则不再处理;如果未处理,则处理消息并存储标识。这样可以保证每条消息只被处理一次,避免了重复消费的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值