简介
- 前一篇为了避免因消息的重复发送导致一个订单的库存归还多次,我们新建了一张表 StockSellDetail
- 其实这里涉及到幂等性,但在此之前,先来了解一些微服务中的常见问题
- 注:微服务不一定是分布式系统,分布式系统也不一定是微服务形式
服务雪崩
- 由于微服务之间相互调用,如果底层服务挂掉或者压力过大,会逐步影响到其他所有服务,导致整个系统崩溃
- 避免雪崩最基本的就是要加调用间的超时机制
- 超时是为了保护服务,但如果 Provider(被调用方) 只是偶尔抖动,那么超时后直接放弃,不做后续处理,就会导致当前请求错误,也会带来业务损失,因此,我们还要加重试机制
- 重试也可能会导致问题,比如产生多个重复消息,如果重复消费会造成损失,这就要考虑幂等性
- 先加入重试机制
- 可以通过中间件的方式实现
- 可以先添加 DialOption,拨号后再给每个接口单独设置 option,因为 grpc 接口默认有
...grpc.CallOption
- 也可以设置并添加 options,再放在 grpc.Dial 里,这样就给所有调用的接口都加了超时重试
var opts []grpc.DialOption // 设置并添加超时重试option retryOpts := []grpc_retry.CallOption{ // 重试3次 grpc_retry.WithMax(3), grpc_retry.WithPerRetryTimeout(1 * time.Second), // 出现这些返回值都会重试 grpc_retry.WithCodes(codes.Unknown, codes.DeadlineExceeded, codes.Unavailable), } opts = append(opts, grpc.WithUnaryInterceptor(grpc_retry.UnaryClientInterceptor(retryOpts...))) conn, err := grpc.Dial("127.0.0.1:50052", opts...) if err != nil { panic(err) } defer conn.Close() c := proto.NewGreeterClient(conn) r, err := c.SayHello(context.Background(), &proto.HelloRequest{Name: "roykun"})
幂等性
- 什么是幂等性
- idempotent、idempotence,是一个数学与计算机学概念,常见于抽象代数中
- 简而言之,就是指一个操作,具有不论执行多少次,产生的效果和返回的结果都是都一样的性质
- 并不是所有接口都要考虑幂等性,一般只需关注 POST/PUT 接口
- 常见的幂等性实现方案
- 设置唯一索引或唯一组合索引
- 悲观锁
- 乐观锁
- 分布式锁
unique index
- 比如在用户操作微服务中,用户表的 Mobile 字段做了如下设置
Mobile string `gorm:"index:idx_mobile;unique;type:varchar(11);not null"`
token
- 有些表是不适合设置唯一索引,比如购物车表,允许用户添加多个,但不能因为网络延迟用户多点了几次就多添加了几件
- 这种情况可以让前端根据当前页面生成 token,存到 redis
- 处理之前先检查 redis 中是否有相同的 token
- 如果没有,存入 redis,继续后面的逻辑
- 注:这种情况是允许用户在新的页面添加相同商品的,这是用户的事,不归程序员管
锁
- 之前通过建表,也就是 select + insert 的方式避免重复归还库存,但这种方式只适合并发不高的系统
- 高并发环境下只能通过加锁的方式
- 悲观锁
- 获取数据的时候加锁
select * from table_xxx where id='xxx' for update;
- id 字段一定要是主键或者唯一索引,不然会锁表
- 放在事务块中才能生效,查询出来后修改数据,事务提交后解锁
- 根据业务逻辑,锁定时间可能会很长,要根据实际情况判断是否使用
- 获取数据的时候加锁
- 乐观锁
- 乐观锁只是在更新数据那一刻锁表,其他时间不锁表,所以相对于悲观锁,效率更高
- 乐观锁的实现方式多种多样,可以通过 version 或者其他限制条件
- 同样的,乐观锁的更新操作,最好用主键或者唯一索引,这样是行锁,否则更新时会锁表
update table_xxx set name=xxx,version=version+1 where id=xxx and version=xxx update table_xxx set avai_amount=avai_amount-xxx where id=xxx and avai_amount-xxx >= 0
- 分布式锁
- 如果是分布是系统,构建全局唯一索引比较困难,此时可以通过第三方的系统(redis或zookeeper)引入分布式锁
- 前面也介绍过,这里不再赘述
小结
- 幂等与是不是分布式高并发没有关系,关键是操作是不是幂等的
- 一个幂等的操作如:把编号为 5 的记录的 A 字段设置为 0,这种操作不管执行多少次都是幂等的
- 一个非幂等的操作如:把编号为 5 的记录的 A 字段增加 1 这种操作显然就不是幂等的
- 要做到幂等性,可以从接口上就不设计任何非幂等的操作
- 譬如说需求是:当用户点赞时,将答案的赞同数量 +1
- 可以改为,当用户点赞时,答案赞同表中增加一条记录:包括用户、答案,要获取赞同数量,让答案赞同表统计
- 应该培养设计接口时考虑幂等性的习惯