go zero微服务处理方法实例分析

发布时间:2022-07-06 09:50:17 作者:iii
来源:亿速云 阅读:166

这篇文章主要介绍“go zero微服务处理方法实例分析”,在日常操作中,相信很多人在go zero微服务处理方法实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”go zero微服务处理方法实例分析”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

处理热点数据

秒杀的数据通常都是热点数据,处理热点数据一般有几种思路:一是优化,二是限制,三是隔离。

优化

优化热点数据最有效的办法就是缓存热点数据,我们可以把热点数据缓存到内存缓存中。

限制

限制更多的是一种保护机制,当秒杀开始后用户就会不断地刷新页面获取数据,这时候我们可以限制单用户的请求次数,比如一秒钟只能请求一次,超过限制直接返回错误,返回的错误尽量对用户友好,比如 "店小二正在忙" 等友好提示。

隔离

秒杀系统设计的第一个原则就是将这种热点数据隔离出来,不要让1%的请求影响到另外的99%,隔离出来后也更方便对这1%的请求做针对性的优化。

具体到实现上,我们需要做服务隔离,即秒杀功能独立为一个服务,通知要做数据隔离,秒杀所调用的大部分是热点数据,我们需要使用单独的Redis集群和单独的Mysql,目的也是不想让1%的数据有机会影响99%的数据。

流量削峰

go zero微服务处理方法实例分析

采用消息队列异步处理后,那么秒杀的结果是不太好同步返回的,所以我们的思路是当用户发起秒杀请求后,同步返回响应用户 "秒杀结果正在计算中..." 的提示信息,当计算完之后我们如何返回结果给用户呢?其实也是有多种方案的。

还有一个问题就是如果异步的请求失败了该怎么办?我觉得对于秒杀场景来说,失败了就直接丢弃就好了,最坏的结果就是这个用户没有抢到而已。如果想要尽量的保证公平的话,那么失败了以后也可以做重试。

如何保证消息只被消费一次

kafka是能够保证"At Least Once"的机制的,即消息不会丢失,但有可能会导致重复消费,消息一旦被重复消费那么就会造成业务逻辑处理的错误,那么我们如何避免消息的重复消费呢?

我们只要保证即使消费到了重复的消息,从消费的最终结果来看和只消费一次的结果等同就好了,也就是保证在消息的生产和消费的过程是幂等的。

什么是幂等呢?

我们可以在消息被消费后,把唯一id存储在数据库中,这里的唯一id可以使用用户id和商品id的组合,在处理下一条消息之前先从数据库中查询这个id看是否被消费过,如果消费过就放弃。伪代码如下:

isConsume := getByID(id)
if isConsume {
  return  
} 
process(message)
save(id)

还有一种方式是通过数据库中的唯一索引来保证幂等性,不过这个要看具体的业务,在这里不再赘述。

代码实现

整个秒杀流程图如下:

go zero微服务处理方法实例分析

使用kafka作为消息队列,所以要先在本地安装kafka,我使用的是mac可以用homebrew直接安装,kafka依赖zookeeper也会自动安装

brew install kafka

安装完后通过brew services start启动zookeeper和kafka,kafka默认侦听在9092端口

brew services start zookeeper
brew services start kafka

seckill-rpc的SeckillOrder方法实现秒杀逻辑,我们先限制用户的请求次数,比如限制用户每秒只能请求一次,这里使用go-zero提供的PeriodLimit功能实现,如果超出限制直接返回

code, _ := l.limiter.Take(strconv.FormatInt(in.UserId, 10))
if code == limit.OverQuota {
  return nil, status.Errorf(codes.OutOfRange, "Number of requests exceeded the limit")
}

接着查看当前抢购商品的库存,如果库存不足就直接返回,如果库存足够的话则认为可以进入下单流程,发消息到kafka,这里kafka使用go-zero提供的kq库,非常简单易用,为秒杀新建一个Topic,配置初始化和逻辑如下:

Kafka:
  Addrs:
    - 127.0.0.1:9092
  SeckillTopic: seckill-topic

 KafkaPusher: kq.NewPusher(c.Kafka.Addrs, c.Kafka.SeckillTopic)

p, err := l.svcCtx.ProductRPC.Product(l.ctx, &product.ProductItemRequest{ProductId: in.ProductId})
if err != nil {
  return nil, err
}
if p.Stock <= 0 {
  return nil, status.Errorf(codes.OutOfRange, "Insufficient stock")
}
kd, err := json.Marshal(&KafkaData{Uid: in.UserId, Pid: in.ProductId})
if err != nil {
  return nil, err
}
if err := l.svcCtx.KafkaPusher.Push(string(kd)); err != nil {
  return nil, err
}

seckill-rmq消费seckill-rpc生产的数据进行下单操作,我们新建seckill-rmq服务,结构如下:

tree ./rmq
./rmq
├── etc
│   └── seckill.yaml
├── internal
│   ├── config
│   │   └── config.go
│   └── service
│       └── service.go
└── seckill.go
4 directories, 4 files

依然是使用kq初始化启动服务,这里我们需要注册一个ConsumeHand方法,该方法用以消费kafka数据

srv := service.NewService(c)
queue := kq.MustNewQueue(c.Kafka, kq.WithHandle(srv.Consume))
defer queue.Stop()
fmt.Println("seckill started!!!")
queue.Start()

在Consume方法中,消费到数据后先反序列化,然后调用product-rpc查看当前商品的库存,如果库存足够的话我们认为可以下单,调用order-rpc进行创建订单操作,最后再更新库存

func (s *Service) Consume(_ string, value string) error {
  logx.Infof("Consume value: %s\n", value)
  var data KafkaData
  if err := json.Unmarshal([]byte(value), &data); err != nil {
    return err
  }
  p, err := s.ProductRPC.Product(context.Background(), &product.ProductItemRequest{ProductId: data.Pid})
  if err != nil {
    return err
  }
  if p.Stock <= 0 {
    return nil
  }
  _, err = s.OrderRPC.CreateOrder(context.Background(), &order.CreateOrderRequest{Uid: data.Uid, Pid: data.Pid})
  if err != nil {
    logx.Errorf("CreateOrder uid: %d pid: %d error: %v", data.Uid, data.Pid, err)
    return err
  }
  _, err = s.ProductRPC.UpdateProductStock(context.Background(), &product.UpdateProductStockRequest{ProductId: data.Pid, Num: 1})
  if err != nil {
    logx.Errorf("UpdateProductStock uid: %d pid: %d error: %v", data.Uid, data.Pid, err)
    return err
  }
  // TODO notify user of successful order placement
  return nil
}

在创建订单过程中涉及到两张表orders和orderitem,所以我们要使用本地事务进行插入,代码如下:

func (m *customOrdersModel) CreateOrder(ctx context.Context, oid string, uid, pid int64) error {
  _, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (sql.Result, error) {
    err := conn.TransactCtx(ctx, func(ctx context.Context, session sqlx.Session) error {
      _, err := session.ExecCtx(ctx, "INSERT INTO orders(id, userid) VALUES(?,?)", oid, uid)
      if err != nil {
        return err
      }
      _, err = session.ExecCtx(ctx, "INSERT INTO orderitem(orderid, userid, proid) VALUES(?,?,?)", "", uid, pid)
      return err
    })
    return nil, err
  })
  return err
}

订单号生成逻辑如下,这里使用时间加上自增数进行订单生成

var num int64
func genOrderID(t time.Time) string {
  s := t.Format("20060102150405")
  m := t.UnixNano()/1e6 - t.UnixNano()/1e9*1e3
  ms := sup(m, 3)
  p := os.Getpid() % 1000
  ps := sup(int64(p), 3)
  i := atomic.AddInt64(&amp;num, 1)
  r := i % 10000
  rs := sup(r, 4)
  n := fmt.Sprintf("%s%s%s%s", s, ms, ps, rs)
  return n
}
func sup(i int64, n int) string {
  m := fmt.Sprintf("%d", i)
  for len(m) &lt; n {
    m = fmt.Sprintf("0%s", m)
  }
  return m
}

最后分别启动product-rpc、order-rpc、seckill-rpc和seckill-rmq服务还有zookeeper、kafka、mysql和redis,启动后我们调用seckill-rpc进行秒杀下单

grpcurl -plaintext -d '{"user_id": 111, "product_id": 10}' 127.0.0.1:9889 seckill.Seckill.SeckillOrder

在seckill-rmq中打印了消费记录,输出如下

{"@timestamp":"2022-06-26T10:11:42.997+08:00","caller":"service/service.go:35","content":"Consume value: {\"uid\":111,\"pid\":10}\n","level":"info"}

这个时候查看orders表中已经创建了订单,同时商品库存减一

go zero微服务处理方法实例分析

到此,关于“go zero微服务处理方法实例分析”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注亿速云网站,小编会继续努力为大家带来更多实用的文章!

推荐阅读:
  1. go-zero 如何应对海量定时/延迟任务
  2. go-zero如何自动管理缓存

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

go zero

上一篇:php如何删除数组前面三个元素

下一篇:怎么使用docker compose搭建etcd集群

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》