最新分布式幂等设计方案_为更新的数据设置前置条件 幂等(1),美团Linux运维研发岗二面

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

这里不仅仅是可以用我们传统的关系型数据库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达到幂等的效果。

最后的话

最近很多小伙伴找我要Linux学习资料,于是我翻箱倒柜,整理了一些优质资源,涵盖视频、电子书、PPT等共享给大家!

资料预览

给大家整理的视频资料:

给大家整理的电子书资料:

如果本文对你有帮助,欢迎点赞、收藏、转发给朋友,让我有持续创作的动力!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以点击这里获取!

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/forums/4f45ff00ff254613a03fab5e56a57acb)**

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值