幂等设计

[TOC]

幂等性,是指同一操作对同一系统的任意多次执行,所产生的影响均与一次执行的影响相同,不会因为多次执行而产生副作用。

在面对系统失败和超时,意外重复,就需要率幂等问题了。尤其是在超时的情况下,并不知道到底是成功了还是失败了,所以幂等设计在服务和接口设计中非常重要。

常见需要考虑幂等的场景:

  • 接口重复提交;
  • MQ消息重复提交,重复消费;
  • 服务超时重试,失败重试。

常见的实现方法有 唯一Token、索引等。它的本质是通过唯一标识,标记同一操作的方式,来消除多次执行的副作用。

当然有些操作天然就有幂等性,比如一些 Get,Delete 操作,执行一次和多次没有区别。

一些幂等操作

接口 GET 请求获取数据

数据库

  • select 查询

  • delete from user where userid=1;
  • update user set money = 100 where userid=1; 不是所有的 update 都是幂等的

全局唯一ID

用全局的唯一ID,来标示操作的唯一性。

那么问题来了:

  • 怎么生成全局唯一ID?
  • 全局唯一ID 由谁来生成和分配?

唯一Id 生成如百度的 uid-generator、美团的 Leaf ,Twitter 的 snowflakeUUID 等。

如果由一个中心系统来分配,会增加了往返性能开销,也容易产生热点问题。

幂等性服务也是分布式的,所以,需要这个存储也是共享的。这样每个服务就变成没有状态的了。但是,这个存储就成了一个非常关键的依赖,其扩展性和可用性也成了非常关键的指标。

UUID

UUID 这样冲突非常小的算法。但 UUID 的问题是,它的字符串占用的空间比较大,索引的效率非常低,生成的 ID 太过于随机,可读性太差,而且没有递增,如果要按前后顺序排序的话,就不太适合了。

import (
	"fmt"
	"github.com/google/uuid"
)

func Uid() {
	for i := 0; i < 10; i++ {
		fmt.Println(uuid.Must(uuid.NewRandom()))
	}
}
//71c96c0d-4d17-4765-aba5-4f6cd1189427
//ba055ed9-4c05-452b-beb0-1a35abf492e1
//c8c39e80-3f0a-41f4-9ca5-9f16d67f9ffc
//0449cbcf-a29a-4632-853c-8da3c85a39be
//a5a89ee0-3a44-472d-8d00-ea4b06c12b09
//749faaa1-17b8-4da5-b715-e2ba953949ba
//d14b0de6-03c9-47b8-8788-858f86b6b055
//9e4c295e-6c3a-48d6-b789-f4588262b89f
//c4f5fdce-b9d6-4c26-851d-32601af34c6d
//0d0ce15a-b0a2-433d-9192-8c1bb5ede2ac

随机生成的 uuid 有122个随机位。一个人每年被陨石击中的概率估计为170亿分之一,每年产生几万亿个uuid 才有一个重复的概率,发生重复概微乎其微。

Snowflake

Twitter 的开源项目 Snowflake, 即雪花算法。它是一个分布式 ID 的生成算法。其核心思想是,产生一个64位整型的 ID。

有三部分组成:

  • 41bits 作为毫秒数。大概可以用 69.7 年。
  • 10bits 作为机器编号(5bits 是数据中心,5bits 的机器 ID),支持 1024 个实例。
  • 12bits 作为毫秒内的序列号。一毫秒可以生成 2^12 也就是 4096 个序号。
+--------------------------------------------------------------------------+
| 1 Bit Unused | 41 Bit Timestamp |  10 Bit NodeID  |   12 Bit Sequence ID |
+--------------------------------------------------------------------------+

Golang 代码实现 https://github.com/bwmarrin/snowflake

每次生成一个ID时,它都是这样工作的。通过 Epoch 指定纪元时间。

  • 使用ID的41位存储毫秒精度的时间戳。
  • 然后将NodeID添加到后续位中。
  • 然后添加序列号,从0开始,并随着同一毫秒内生成的每个ID而递增。如果同一毫秒内生成了超过 4096 个id,使得序列将滚过或满溢,那么ID 生成器函数将暂停,直到下一毫秒。

通过设置雪花,可以更改节点id和序列号的位数。这两个值之间最多可以共享22位。你不必使用所有的22位。

import (
	"fmt"
	"github.com/bwmarrin/snowflake"
)

func Sn1(nodeId int64) {
	// Create a new Node with a Node number of 1
	node, err := snowflake.NewNode(nodeId)
	if err != nil {
		fmt.Println(err)
		return
	}

	for i := 0; i < 5; i++ {
		// Generate a snowflake ID.
		id := node.Generate()
		// Print out the ID in a few different ways.
		fmt.Printf("Int64  ID: %d\n", id)
	}
	fmt.Println()
}

//Int64  ID: 1358242168543645696
//Int64  ID: 1358242168543645697
//Int64  ID: 1358242168543645698
//Int64  ID: 1358242168543645699
//Int64  ID: 1358242168543645700

优点:

  • 按照时间自增,并且根据节点区分分布式系统内不会产生ID碰撞,并且效率较高。
  • 实现简单,无第三方依赖。
  • 可以根据业务特点灵活调整 bit 分配。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复。

具体实现

Token 机制

每次请求生成唯一的 Token 来区分请求。

数据库唯

数据库唯一索引

最简单的方式就是数据库字段的唯一索引。

插入或更新

INSERT INTO  values  ON DUPLICATE KEY UPDATE 

多版本控制

就可以在更新的接口中增加一个版本号,来做幂等。

在操作业务前,需要先查询出当前的 version 版本,再更新,这个利用了乐观锁的思路。但是乐观锁在面临并发竞争的时候,都要面对失败的情况。

UPDATE goods SET name = ${newName}, version = ${newVersion} WHERE id = S{id} AND version < ${newVersion}

状态机

通过状态流转实现幂等、

如订单的创建和付款,订单的付款肯定是在之前,设计 int 型状态字段时,并通过值类型的大小来做幂等。

订单创建成功为 0,已付款为状态 1,商家已接单为状态20,骑手接单为状态 40,骑手已取货为 60,已签收为 80,订单完成为 100,等等。

在做状态机更新时,我们就这可以这样控制:

UPDATE `order` SET status = S{status} WHERE id = ${id} AND status < s{status}

这个其实也算是版本控制。

Redis 唯一键

基于 SETNX 命令.

SETNX key value, 将 key 的值设为 value ,当且仅当 key 不存在才能设置成功。

HTTP 的幂等性

满足幂等

  • HTTP GET 方法用于获取资源,不应有副作用,所以是幂等的。需要注意的是,GET一次和 N 次具有相同的副作用,而不是每次 GET 的结果相同。

  • HTTP HEAD 和 GET 本质是一样的,区别在于 HEAD 不含有呈现数据,而仅仅是 HTTP 头信息,不应用有副作用,也是幂等的。

  • HTTP OPTIONS 主要用于获取当前 URL 所支持的方法,所以也是幂等的。

  • HTTP DELETE 方法用于删除资源,有副作用,但它应该满足幂等性。
  • HTTP PUT 方法用于创建或更新操作,所对应的 URI 是要创建或更新的资源本身,有副作用,但满足幂等性。

不满足幂等性

  • HTTP POST 方法用于创建资源,所对应的 URI 并非创建的资源本身,而是去执行创建动作的操作者,有副作用,不满足幂等性。

POST 幂等设计

1、在表单中需要隐藏一个 token,这个 token 可以是前端生成的一个唯一的 ID。这个 token 是表单的唯一标识, 用于防止表单重复提交。(这种情况其实是通过前端生成唯一ID 把 POST 变成了 PUT)。

提交后,后端会把用户提交的数据和这个 token 保存在数据库中。如果有重复提交,那么数据库中的 token 会做排它限制,从而做到幂等性。

2、Post/Redirect/Get (PRG)模式。

默认情况,提交 Post 请求到服务器后,如果直接刷新浏览器,会重新再提交一次 Post 请求,这样就会产生重复提交问题。

当服务器处理完 Post 请求后,通过重定向,让浏览器用 Get 方式立刻访问另一条 URL,用户浏览器拿到 Get 请求的数据,整个流程才算结束。此时用户刷新当前页面,也不会引起 Post 请求的重复提交了。

此外,Post 请求直接返回的网页收藏到书签是无效的,采用 PRG 方式,用户请求重定向后 GET 方法返回的网页,这样使得收藏有效了。

分布式环境

很多时候,需要在一个分布式环境多个节点之间满足幂等性,这就需要一个公共的存储。这样可以避免单个节点的状态。但是,这个存储就成了一个非常关键的依赖,其扩展性可用性也成了非常关键的指标。

这是可以考虑使用KV数据库,ZAB,Raft,Quorum NWR 算法结合数据分片,来实现强一致和高可用。

也可以直接把唯一ID与数据一起存储。比如交易流水号。

小结

变来变去,其实最核心的就是通过一个唯一表示来区分,请求的唯一性。

更新时间: