微服务架构设计的中心思想是将服务进行拆分,但是在这个过程中,如果被依赖的服务发生奔溃,就会引起一系列问题。为了解决这个问题,就会引入重试的机制,重试又会引入幂等性的问题,下面我们就分析这个过程,然后探讨一下常见的解决方案。
比如:新建用户的时候,用手机号作为唯一索引,那么即使重试,也会只新建一个用户,不会因为多次重试导致当前用户被注册了很多次。
在前端提交之前,生成一个token,在提交以后,可以判断该token是否已经进行了处理,这样可以防止数据的重复提交。token的特点:要申请,一次有效性,可以限流
注意:如果要用redis校验token,建议使用redis删除来判断token,删除成功代表token校验通过,如果采用select + delete来校验token,由于操作redis的次数多,存在并发问题,所以不建议使用。
获取数据时加锁,其他数据会被阻塞在这里,执行完成后再判断是否已经提交过,这样可以防止多次提交,但是性能不太好,数据锁定的时间可能会很长,根据实际情况选用。
根据数据库数据增加版本号的方式或者通过限制条件增加乐观锁,和悲观锁的原理一样,但是不会锁住表。
用redis等中间件做分布式锁,可以防止并发操作,如果已经存在这个数据,其他提交就可以不用再进行操作了。
对于并发不太高的系统,可以采用先查询一下,如果存在,就不用再操作的方式,实现幂等性,但是并发高的核心系统不能这样做,因为没有加锁,insert操作可能会被多次执行。
如银联提供的付款接口,需要接入商户提交付款请求时附带source来源,seq序列号等,用source + seq在数据库中做唯一的索引,防止多次付款。
在grpc中,可以在调用时增加grpc.DialOption的方式,来实现超时重试的机制。
在用proto生成的接口中,调用的时候,可以增加grpc.CallOption的调用参数,我们可以在这里增加重试的功能:
type UserClient interface {
GetUserList(ctx context.Context, in *PageInfo, opts ...grpc.CallOption) (*UserListResponse, error)
GetUserByMobile(ctx context.Context, in *MobileRequest, opts ...grpc.CallOption) (*UserInfoResponse, error)
GetUserById(ctx context.Context, in *IdRequest, opts ...grpc.CallOption) (*UserInfoResponse, error)
CreateUser(ctx context.Context, in *CreateUserInfo, opts ...grpc.CallOption) (*UserInfoResponse, error)
UpdateUser(ctx context.Context, in *UpdateUserInfo, opts ...grpc.CallOption) (*emptypb.Empty, error)
CheckPassWord(ctx context.Context, in *PasswordCheckInfo, opts ...grpc.CallOption) (*CheckResponse, error)
}
在接口中给定的参数,只是对这个接口起作用,要想让所有的调用都起作用,我们可以在进行连接的时候,就指定grpc.DialOption,这样对这个连接中的接口,都会起作用。
Dial方法的声明如下:
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
return DialContext(context.Background(), target, opts...)
}
下面是我们在连接时指定重试的示例代码实现:
import (
"context"
"fmt"
"time"
"google.golang.org/grpc/codes"
grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
"google.golang.org/grpc"
"OldPackageTest/grpc_test/proto"
)
func main() {
// 增加一个耗时打印的interceptor
interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
start := time.Now()
err := invoker(ctx, method, req, reply, cc, opts...)
fmt.Printf("耗时:%s\n", time.Since(start))
return err
}
var opts []grpc.DialOption
opts = append(opts, grpc.WithInsecure())
retryOpts := []grpc_retry.CallOption{
grpc_retry.WithMax(3),// 最大的重试次数
grpc_retry.WithPerRetryTimeout(13 * time.Second),//超时时间
grpc_retry.WithCodes(codes.Unknown, codes.DeadlineExceeded, codes.Unavailable),// 对于哪些返回状态进行重试
}
opts = append(opts, grpc.WithUnaryInterceptor(interceptor))
//这个请求应该多长时间超时, 这个重试应该几次、当服务器返回什么状态码的时候重试
opts = append(opts, grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(retryOpts...)))
conn, err := grpc.Dial("127.0.0.1:50051", opts...)
if err != nil {
panic(err)
}
defer conn.Close()
c := proto.NewGreeterClient(conn)
r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "bobby"})
if err != nil {
panic(err)
}
fmt.Println(r.Message)
}
后记
个人总结,欢迎转载、评论、批评指正