IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 如何进行幂等设计 -> 正文阅读

[大数据]如何进行幂等设计

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

幂等设计作为网络通信重要的一环,在消息队列的使用中、在接口调用的场景中,我们或多或少的都要对自己提供的接口进行幂等设计。当我们的接口涉及到金额相关的操作。我们一定要考虑幂等,不然就可能存在。退款多退、订单多下、佣金多发等重大生产事故。


提示:以下是本篇文章正文内容,下面案例可供参考

一、幂等是什么?

幂等其实是数学概念,它的定义是这样的:
如果一个函数 f(x) 满足:f(f(x)) = f(x),则函数 f(x) 满足幂等性。

回到我们的软件设计的过程中:
例如:我们提供出去的接口:被调用一次和调用10次,调用100次,对业务执行的结果都是一样的。
比如: 我们设计这样的接口给达到要去的销售人员,发送佣金。
假如每人发送100元,如果接口支持幂等,那么调用方调用该接口,无论调用多少次。销售人员的账户都只会发送100元的佣金,但是如果不支持幂等,那么就有可能造成销售人员的账户发放100*N(N指调用次数)的佣金。

二、幂等设计的场景

1.多次调用对业务有影响的接口

理论上针对多次调用对业务有影响的接口都需要支持幂等。
那如何去判断,上述例子我们描述了一个需要幂等的场景。
接下里我们介绍一种不需要幂等的场景:
假如我们要去删除一个订单数据接口:这种场景就不需要考虑幂等。
因为对于查询、删除这样的操作是天然的幂等的操作。
对于更新和插入操作,我们就需要考虑幂等,我们是否会更新多次数据或者插入多条数据。


2.消费消息队列中间件的接口

我们现在常用的绝大部分消息队列提供的服务质量都是 At least once(至少一次,可能会推送多次),包括 RocketMQ、RabbitMQ 和 Kafka 都是这样。也就是说,消息队列并不会保证消息不超不重复。
所以我们在设计这样的接口的时候一定要支持幂等,不然就可能造成业务影响。

三、如何幂等设计

接下来我总结了几种幂等设计的思路与方案

1、利用数据库唯一约束

针对插入数据的场景,我们就可以数据库的唯一约束来。例如为我们业务操作生成一个业务主键、当第一次请求的时候,我们保存的数据库保存成功、当第二次请求用同一个主键、保存数据就会失败。
在这里插入图片描述

这里不仅仅是可以用我们传统的关系型数据库Mysql/Oracle等,在非关系型数据库比如redis也同样适用。比如SetNx
但是SetNx需要考率缓存过期的问题,比如sentNx的缓存为10分钟,但是我们两次请求间隔超过10分钟。
在这里插入图片描述
Redis锁的实现代码

@Component
public class RedisLock {
 
  private static final Long RELEASE_SUCCESS = 1L;
  private static final String LOCK_SUCCESS = "OK";
  private static final String SET_IF_NOT_EXIST = "NX";
  // 当前设置 过期时间单位, EX = seconds; PX = milliseconds
  private static final String SET_WITH_EXPIRE_TIME = "EX";
  //lua
  private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
 
  @Autowired
  private StringRedisTemplate redisTemplate;
 
 
  /**
   * 该加锁方法仅针对单实例 Redis 可实现分布式加锁
   * 对于 Redis 集群则无法使用
   * <p>
   * 支持重复,线程安全
   *
   * @param lockKey  加锁键
   * @param clientId 加锁客户端唯一标识(采用UUID)
   * @param seconds  锁过期时间
   * @return
   */
  public boolean tryLock(String lockKey, String clientId, long seconds) {
      return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
//            Jedis jedis = (Jedis) redisConnection.getNativeConnection();
          Object nativeConnection = redisConnection.getNativeConnection();
          RedisSerializer<String> stringRedisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
          byte[] keyByte = stringRedisSerializer.serialize(lockKey);
          byte[] valueByte = stringRedisSerializer.serialize(clientId);
 
          // lettuce连接包下 redis 单机模式
          if (nativeConnection instanceof RedisAsyncCommands) {
              RedisAsyncCommands connection = (RedisAsyncCommands) nativeConnection;
              RedisCommands commands = connection.getStatefulConnection().sync();
              String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
              if (LOCK_SUCCESS.equals(result)) {
                  return true;
              }
          }
          // lettuce连接包下 redis 集群模式
          if (nativeConnection instanceof RedisAdvancedClusterAsyncCommands) {
              RedisAdvancedClusterAsyncCommands connection = (RedisAdvancedClusterAsyncCommands) nativeConnection;
              RedisAdvancedClusterCommands commands = connection.getStatefulConnection().sync();
              String result = commands.set(keyByte, valueByte, SetArgs.Builder.nx().ex(seconds));
              if (LOCK_SUCCESS.equals(result)) {
                  return true;
              }
          }
 
          if (nativeConnection instanceof JedisCommands) {
              JedisCommands jedis = (JedisCommands) nativeConnection;
              String result = jedis.set(lockKey, clientId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, seconds);
              if (LOCK_SUCCESS.equals(result)) {
                  return true;
              }
          }
          return false;
      });
  }
 
  /**
   * 与 tryLock 相对应,用作释放锁
   *
   * @param lockKey
   * @param clientId
   * @return
   */
  public boolean releaseLock(String lockKey, String clientId) {
      DefaultRedisScript<Integer> redisScript = new DefaultRedisScript<>();
      redisScript.setScriptText(RELEASE_LOCK_SCRIPT);
      redisScript.setResultType(Integer.class);
//        Integer execute = redisTemplate.execute(redisScript, Collections.singletonList(lockKey), clientId);
 
      Object execute = redisTemplate.execute((RedisConnection connection) -> connection.eval(
              RELEASE_LOCK_SCRIPT.getBytes(),
              ReturnType.INTEGER,
              1,
              lockKey.getBytes(),
              clientId.getBytes()));
      if (RELEASE_SUCCESS.equals(execute)) {
          return true;
      }
      return false;
  }
}

2、Http的幂等

  • GET:只是获取资源,对资源本身没有任何副作用,天然的幂等性。 HEAD:本质上和GET一样,获取头信息,主要是探活的作用,具有幂等性。
  • OPTIONS:获取当前URL所支持的方法,因此也是具有幂等性的。
  • DELETE:用于删除资源,有副作用,但是它应该满足幂等性,比如根据id删除某一个资源,调用方可以调用N次而不用担心引起的错误(根据业务需求而变)。
  • PUT:用于更新资源,有副作用,但是它应该满足幂等性,比如根据id更新数据,调用多次和N次的作用是相同的(根据业务需求而变)。
  • POST:用于添加资源,多次提交很可能产生副作用,比如订单提交,多次提交很可能产生多笔订单。

3、为更新的数据设置前置条件(乐观锁)

当我们更新数据的时候。我们可以使用乐观锁的思想:
例如我们有这样的一种需求,我们需要给账户A转100块钱,这种场景,我们显然是需要支持幂等。我们正常调用多次,可能就会给A转多的钱。当我们转变思想,我们在更新前查询出A账户目前的余额,假设是200.所以我们的的更新语句就可以是:update XX set account = account +100 where account =200 ,这样就支持了幂等,只有在账户余额为200的情况下,我们才能更新成功、那么这样操作手法只能是数值的情况,其实我们变通下即使不是数据,我们也可以用vesion 版本号来替代上面的账户余额。这样什么样的数据都可以通用了。

4、记录并检查操作 (Token)

还有一种通用的手法就是Token机制:
在这里插入图片描述

主要的流程步骤如下:

  • 客户端先发送获取token的请求,服务端会生成一个全局唯一的ID保存在redis中,同时把这个ID返回给客户端。
  • 客户端调用业务请求的时候必须携带这个token,一般放在请求头上。
  • 服务端会校验这个Token,如果校验成功,则执行业务。
  • 如果校验失败,则表示重复操作,直接返回指定的结果给客户端。

通过以上的流程分析,唯一的重点就是这个全局唯一ID如何生成,在分布式服务中往往都会有一个生成全局ID的服务来保证ID的唯一性,但是工程量和实现难度比较大,UUID的数据量相对有些大,就可以选择facebook的雪花算法、美团的leaf算法。注意雪花算法存在时间倒拨的问题。

5、状态机算法

  • 很多业务中多有多个状态,比如订单的状态有提交、待支付、已支付、取消、退款等等状态。后端可以根据不同的状态去保证幂等性,比如在退款的时候,一定要保证这笔订单是已支付的状态。
  • 业务中常常出现,有兴趣的同学可以自行去了解下,后续我在补充具体的逻辑

总结

  • 针对数据插入场景:可以选择使用数据库唯一约束的方案
  • 针对数据更新的场景:可以选择为更新数据设置前置条件(数据库乐观锁 Vesion等)
  • 针对Http请求,可以使用PUT代替POST达到幂等的效果。
  • 比较通用的方案:可以选择使用 TOKEN机制、状态机模式

参考文章:

https://blog.csdn.net/qq_34162294/article/details/105252181
https://time.geekbang.org/column/article/111552

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-06-16 21:45:45  更:2022-06-16 21:48:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 0:35:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码