gRPC重試與介面冪等性

BigSun丶發表於2024-03-19

目錄
  • 一、gRPC超時重試
    • 1.1 客戶端
    • 1.2 服務端
    • 1.3 proto
  • 二、介面冪等性
    • 2.1 什麼是冪等性
    • 2.2 什麼情況下需要冪等
    • 2.3 如何保證冪等
      • (1)token機制
      • (2)關鍵點 先刪除token,還是後刪除token
      • (3)token機制缺點
      • (4)樂觀鎖機制
      • (5)唯一主鍵
      • (6)防重表
      • (7)唯一ID
      • (8)唯一ID機制

一、gRPC超時重試

  • 使用開源的;https://github.com/grpc-ecosystem/go-grpc-middleware

image-20220612005951908

1.1 客戶端

package main

import (
	"context"
	"fmt"
	retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
	"go_test_learn/grpc_retry/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/credentials/insecure"
	"time"
)

func main() {
	// 建立客戶端攔截器
	interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error{
		start := time.Now()
		fmt.Println("客戶端攔截器")
		err := invoker(ctx, method, req, reply, cc, opts...)
		fmt.Printf("耗時:%s\n", time.Since(start))
		return err
	}
	opt := grpc.WithUnaryInterceptor(interceptor)

	//********方式二;在此處配置******
	retryOpt:=[]retry.CallOption{
		retry.WithMax(3), //重試3次
		retry.WithPerRetryTimeout(1*time.Second), // 超過1s就要重試
		retry.WithCodes(codes.Unknown,codes.DeadlineExceeded,codes.Unavailable), // 哪些狀態碼重試
	}
	conn, err := grpc.Dial("127.0.0.1:50052",
		opt,
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		// 客戶端攔截器中加入retry--》多長時間超時?重試幾次?
		// 方式一:配合下面
		//grpc.WithUnaryInterceptor(retry.UnaryClientInterceptor()),

		// 方式二:配置上面
		grpc.WithUnaryInterceptor(retry.UnaryClientInterceptor(retryOpt...)),
		)
	if err != nil {
		panic("連線服務異常")
	}
	defer conn.Close()
	client := proto.NewHelloClient(conn)
	request := proto.HelloRequest{Name: "lqz",}
	// **********方式一:在此處配置重試超時時間和重試次數和錯誤狀態碼********
	//res, err := client.Hello(context.Background(),
	//	&request,
	//	retry.WithMax(3), //重試3次
	//	retry.WithPerRetryTimeout(3*time.Second), // 超過1s就要重試
	//	retry.WithCodes(codes.Unknown,codes.DeadlineExceeded,codes.Unavailable), // 哪些狀態碼重試
	//)

	//*****方式二;
	res, err := client.Hello(context.Background(),
		&request,
	)
	if err != nil {
		panic("呼叫方法異常")
	}

	fmt.Println(res.Reply)
}

1.2 服務端

package main

import (
	"context"
	"fmt"
	"github.com/grpc-ecosystem/go-grpc-middleware"
	grpc_auth "github.com/grpc-ecosystem/go-grpc-middleware/auth"
	grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
	"go_test_learn/grpc_retry/proto"
	"google.golang.org/grpc"
	"net"
	"time"
)

type HelloServer struct {
}

func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
	fmt.Println(request.Name)
	time.Sleep(2*time.Second)
	return &proto.HelloResponse{
		Reply: "收到客戶端的資料:" + request.Name,
	}, nil
}

func main() {
	// 使用第三方攔截器,使用了grpc_auth和grpc_recovery
	myAuthFunction := func(ctx context.Context) (context.Context, error) {
		fmt.Println("走了認證")
		return ctx, nil
	}
	opt := grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
		grpc_auth.UnaryServerInterceptor(myAuthFunction),
		grpc_recovery.UnaryServerInterceptor(),
	))
	g := grpc.NewServer(opt)
	// 使用攔截器結束
	s := HelloServer{}
	proto.RegisterHelloServer(g, &s)
	lis, error := net.Listen("tcp", "0.0.0.0:50052")
	if error != nil {
		panic("啟動服務異常")
	}
	g.Serve(lis)

}

1.3 proto

syntax = "proto3";
option go_package = ".;proto";


service Hello{
  rpc Hello(HelloRequest) returns(HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string reply = 1;
}

二、介面冪等性

2.1 什麼是冪等性

冪等性是系統服務對外一種承諾,承諾只要呼叫介面成功,外部多次呼叫對系統的影響是一致的。宣告為冪等的服務會認為外部呼叫失敗是常態,並且失敗之後必然會有重試。

2.2 什麼情況下需要冪等

  • 以SQL為例:
    • SELECT col1 FROM tab1 WHER col2=2,無論執行多少次都不會改變狀態,是天然的冪等。
    • UPDATE tab1 SET col1=1 WHERE col2=2,無論執行成功多少次狀態都是一致的,因此也是冪等操作。
    • UPDATE tab1 SET col1=col1+1 WHERE col2=2,每次執行的結果都會發生變化,這種不是冪等的。
    • insert into user(userid,name) values(1,’a’) 如userid為唯一主鍵,即重複操作上面的業務,只會插入一條使用者資料,具備冪等性。
    • 如userid不是主鍵,可以重複,那上面業務多次操作,資料都會新增多條,不具備冪等性。
    • delete from user where userid=1,多次操作,結果一樣,具備冪等性

2.3 如何保證冪等

(1)token機制

1、服務端提供了傳送token的介面。我們在分析業務的時候,哪些業務是存在冪等問題的,就必須在執行業務前,先去獲取token,伺服器會把token儲存到redis中。

2、然後呼叫業務介面請求時,把token攜帶過去,一般放在請求頭部。

3、伺服器判斷token是否存在redis中,存在表示第一次請求,然後刪除token,繼續執行業務。

4、如果判斷token不存在redis中,就表示是重複操作,直接返回重複標記給client,這樣就保證了業務程式碼,不被重複執行。

(2)關鍵點 先刪除token,還是後刪除token

  • 後刪除token:如果進行業務處理成功後,刪除redis中的token失敗了,這樣就導致了有可能會發生重複請求,因為token沒有被刪除。這個問題其實是資料庫和快取redis資料不一致問題,後續會寫文章進行講解。

  • 先刪除token:如果系統出現問題導致業務處理出現異常,業務處理沒有成功,介面呼叫方也沒有獲取到明確的結果,然後進行重試,但token已經刪除掉了,服務端判斷token不存在,認為是重複請求,就直接返回了,無法進行業務處理了。

  • 先刪除token可以保證不會因為重複請求,業務資料出現問題。出現業務異常,可以讓呼叫方配合處理一下,重新獲取新的token,再次由業務呼叫方發起重試請求就ok了。

(3)token機制缺點

  • 業務請求每次請求,都會有額外的請求(一次獲取token請求、判斷token是否存在的業務)。其實真實的生產環境中,1萬請求也許只會存在10個左右的請求會發生重試,為了這10個請求,我們讓9990個請求都發生了額外的請求。

(4)樂觀鎖機制

  • 這種方法適合在更新的場景中,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條件就不成立。這樣就保證了不管呼叫幾次,只會真正的處理一次。
  • 樂觀鎖主要使用於處理讀多寫少的問題

(5)唯一主鍵

  • 這個機制是利用了資料庫的主鍵唯一約束的特性,解決了在insert場景時冪等問題。但主鍵的要求不是自增的主鍵,這樣就需要業務生成全域性唯一的主鍵。

  • 如果是分庫分表場景下,路由規則要保證相同請求下,落地在同一個資料庫和同一表中,要不然資料庫主鍵約束就不起效果了,因為是不同的資料庫和表主鍵不相關。

(6)防重表

  • 使用訂單號orderNo做為去重表的唯一索引,把唯一索引插入去重表,再進行業務操作,且他們在同一個事務中。這個保證了重複請求時,因為去重表有唯一約束,導致請求失敗,避免了冪等問題。這裡要注意的是,去重表和業務表應該在同一庫中,這樣就保證了在同一個事務,即使業務操作失敗了,也會把去重表的資料回滾。這個很好的保證了資料一致性。

(7)唯一ID

  • 呼叫介面時,生成一個唯一id,redis將資料儲存到集合中(去重),存在即處理過。

(8)唯一ID機制

  • 呼叫者生成一個唯一ID

相關文章