{"id":20366329,"url":"https://github.com/ni-ning/charites","last_synced_at":"2026-05-07T21:44:41.022Z","repository":{"id":65101313,"uuid":"581075649","full_name":"ni-ning/charites","owner":"ni-ning","description":"微服务gRPC项目集合-直播电商","archived":false,"fork":false,"pushed_at":"2023-01-02T14:18:03.000Z","size":193,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-15T05:04:39.595Z","etag":null,"topics":["go","golang","grpc"],"latest_commit_sha":null,"homepage":"https://ni-ning.cn/golangv2/shopping.html","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ni-ning.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-12-22T07:57:32.000Z","updated_at":"2024-12-08T14:13:41.000Z","dependencies_parsed_at":"2023-02-01T03:01:10.964Z","dependency_job_id":null,"html_url":"https://github.com/ni-ning/charites","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ni-ning%2Fcharites","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ni-ning%2Fcharites/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ni-ning%2Fcharites/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ni-ning%2Fcharites/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ni-ning","download_url":"https://codeload.github.com/ni-ning/charites/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241908132,"owners_count":20040586,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","grpc"],"created_at":"2024-11-15T00:24:03.701Z","updated_at":"2026-05-07T21:44:40.914Z","avatar_url":"https://github.com/ni-ning.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 微服务gRPC项目集-直播电商\n\n`gRPC`框架从0到1搭建微服务平台，包括商品、库存与订单微服务，归纳总结核心技术点：\n\n- \u003ca href=\"/golangv2/shopping.html#_1-项目背景说明\"\u003e1. 项目背景说明\u003c/a\u003e\n- \u003ca href=\"/golangv2/shopping.html#_2-项目结构搭建\"\u003e2. 项目结构搭建\u003c/a\u003e\n- \u003ca href=\"/golangv2/shopping.html#_3-创建库表模型\"\u003e3. 创建库表模型\u003c/a\u003e\n- \u003ca href=\"/golangv2/shopping.html#_4-商品微服务\"\u003e4. 商品微服务\u003c/a\u003e\n    * 创建库表模型\n    * 编写proto文件\n    * 生成proto代码\n    * 实现服务端代码\n    * gPRC-Gateway\n    * Makefile 快速实现\n- \u003ca href=\"/golangv2/shopping.html#_5-库存微服务\"\u003e5. 库存微服务\u003c/a\u003e\n    * 通用业务开发流程\n    * 测试方法汇总\n    * 并发资源竞争示例\n    * 事务处理示例\n    * 悲观锁实现并发\n    * 乐观锁实现并发\n    * 分布式锁实现并发\n- \u003ca href=\"/golangv2/shopping.html#_6-订单微服务\"\u003e6. 订单微服务\u003c/a\u003e\n\t* 微服务相互调用\n    * 雪花算法订单号\n    * 创建订单直接版\n- \u003ca href=\"/golangv2/shopping.html#_7-分布式事务\"\u003e7. 分布式事务\u003c/a\u003e\n\t* 分布式事务介绍\n- \u003ca href=\"/golangv2/shopping.html#_8-rocketmq入门\"\u003e8. Rocketmq入门\u003c/a\u003e\n\t* 本地安装RocketMQ\n\t* Go语言客户端\n- \u003ca href=\"golangv2/shopping.html#_9-分布式订单\"\u003e9. 分布式订单\u003c/a\u003e\n\t* 本地事务订单逻辑\n\t* 库存微服务回滚\n\t* 订单未支付延时消息\n- \u003ca href=\"/golangv2/shopping.html#_10-本地部署启动\"\u003e10. 本地部署启动\u003c/a\u003e\n- 源码地址 [https://github.com/ni-ning/charites](https://github.com/ni-ning/charites)\n\n## 1. 项目背景说明\n\n- 常规直播电商业务与架构\n```js\n1. 直播\n直播技术架构涉及很多方面\n    1. 推拉流\n        1. 推流端把实时的音视频数据推送到服务端，服务端（合流、转码、录制、转推、鉴黄）\n        2. 拉流 看播\n        3. 连麦PK --\u003e 实时性要求很高\n        4. 多人语音房，9连麦\n        5. obs推流 --\u003e 大型活动，专业音视频设备\n    2. 技术栈：\n        1. 前端：h5、ios、android  sdk\n        2. 后端：C++、播放器、ffmpeg、webrtc、cdn\n    3. 业务类\n        1. webrtc 信令模块，任务模块\n        2. im ：直播间聊天、私信等--\u003e goim\n        3. 点赞、送礼、排行榜、粉丝标签等等\n\n2. 电商\n电商业务涉及很多方面，包括无实物、有实物、O2O、B2C等，大型的电商架构\n    1. 商品中心: SPU --\u003e 品 (iphone13)、SKU --\u003e item(iphone13 金色 128G)、类目中心\n    2. 库存: 单一仓库、分区仓库\n    3. 商户中心: 大品牌、经销商、核销、广告\n    4. 订单中心: 订单、购物车\n    5. 支付中心: 支付方式、定期支付、定金支付、混合支付、货到付款\n    6. 物流中心: 寄快递、查快递\n    7. 履约中心: 退货、换货、只退不换\n    8. 用户中心: 地址服务、收藏服务、推荐\n    9. 营销中心: 优惠券、满减券、折扣券、专属券、平台会员、店铺会员\n    10. 广告推荐:\n    11. 发票\n```\n完整的直播需要专门的音视频团队，或者采用三方的集成方案\n\n本项目只实现部分微服务，以`打通后端架构，实践新技术`为目标，具体包括\n1. `商品微服务` 侧重gRPC实现周边\n2. `库存微服务` 侧重并发锁实现\n3. `订单微服务` 侧重分布式事务实现\n\n\n## 2. 项目结构搭建\n```js\ncharites\n    |- apps    // 实际项目中会拆分为不同的微服务项目\n        |- order        // 订单微服务\n        |- shoppig      // 商品微服务\n        |- stock        // 库存微服务\n    |- bootstrap    // 初始化各类配置\n        |- init.go\n        |- logger.go\n        |- mysql.go\n        |- redis.go\n        |- rpc.go\n        |- setting.go\n        |- snowflake.go\n    |- client   // 可作为测试客户端\n    |- config   // 配置文件，如Server、App、Database\n        |- config.yaml\n    |- global       // 全局变量，如配置、数据库连接、日志\n    |- middleware   // 拦截器，包括客户端和服务端\n    |- model        // 模型数据\n    |- pkg          // 项目公共模块\n        |- errcode  // 错误码，定义NewError，自定义业务错误码\n        |- logger   // 日志，定义NewLogger，结合初始化函数，得到全局变量\n        |- registry // 注册服务中心封装工具\n        |- setting  // 配置，定义NewSetting，结合初始化函数，得到全局变量\n\t\t|- utils    // 如获取出口IP\n    |- proto        // gRPC 定义的传输模型 Protobuf\n    |- storage\n\t\t|- sql\t    // SQL文件\n\t\t|- logs\t\t// 日志文件\n    |- main.go\n    |- Makefile     // 编译快捷命令\n```\n\n\n## 3. 创建库表模型\n\n- 创建数据库表 `storage/sql/*.sql`\n- 创建模型 `model/*.go`\n- 库表直接关联关系\n\n![](./assets/model.png)\n\n\n## 4. 商品微服务\n\n`gRPC`实现直播间商品列表时，需要的各类技术点\n\n\n### 编写proto文件\n```js\nsyntax = \"proto3\";\n\noption go_package = \".;proto\";\n\nservice Goods {\n    // 获取直播间商品列表\n    rpc GetGoodsListByRoomId(GetGoodsListRoomReq) returns (GoodsListReply){};\n\n    // 获取商品详情\n    rpc GetGoodsDetail(GetGoodsDetailReq) returns (GoodsDetailReply){};\n}\n\nmessage GetGoodsListRoomReq{\n    int64 RoomId = 2;\n}\n\nmessage GoodsInfo {\n    int64 GoodsId = 1;\n    int64 CategoryId = 2;\n    int32 Status = 3;\n    string Title = 4;\n    string MarketPrice = 5;\n    string Price = 6;\n    string Brief = 7;\n    repeated string HeadImgs = 8;\n}\n\nmessage GoodsListReply {\n    int64 CurrentGoodsId = 1;\n    repeated GoodsInfo data = 2 ;\n}\n\nmessage GetGoodsDetailReq {\n    int64 GoodsId = 1;\n}\n\nmessage GoodsDetailReply {\n    int64 GoodsId = 1;\n    int64 CategoryId = 2;\n    int32 Status = 3;\n    int64 Code = 4;\n    string BrandName = 5;\n    string Title = 6;\n    string MarketPrice = 7;\n    string Price = 8;\n    string Brief = 9;\n    repeated string HeadImgs = 10;\n    repeated string Videos = 11;\n    repeated string Detail = 12;\n}\n```\n\n### 生成proto代码\n```js\nprotoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./proto/*.proto\n```\n\n### 实现服务端代码\n```go\ntype GoodsServer struct {\n\tpb.UnimplementedGoodsServer\n}\n\nfunc NewGoodsServer() *GoodsServer {\n\treturn \u0026GoodsServer{}\n}\n\nfunc (g GoodsServer) GetGoodsListByRoomId(context.Context, *pb.GetGoodsListRoomReq) (*pb.GoodsListReply, error) {\n\treturn nil, errcode.ToRPCError(errcode.Success)\n}\nfunc (g GoodsServer) GetGoodsDetail(context.Context, *pb.GetGoodsDetailReq) (*pb.GoodsDetailReply, error) {\n\treturn nil, errcode.ToRPCError(errcode.Success)\n}\n```\n\n### gPRC-Gateway\n\n[grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway) 是一个`protoc`插件，生成一个反向代理服务器，实现通过`RESTful API`访问`gRPC`服务\n\n![](./assets/gateway.png)\n\n- 安装依赖库与插件\n```go\n// +build tools\npackage tools\n\nimport (\n    _ \"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway\"\n    _ \"github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2\"\n    _ \"google.golang.org/grpc/cmd/protoc-gen-go-grpc\"\n    _ \"google.golang.org/protobuf/cmd/protoc-gen-go\"\n)\n\n go install \\\n    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \\\n    github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \\\n    google.golang.org/protobuf/cmd/protoc-gen-go \\\n    google.golang.org/grpc/cmd/protoc-gen-go-grpc\n```\n\n- 安装三方proto\n```js \n// 下载 google 预定义proto文件 \nhttps://github.com/googleapis/googleapis/blob/master/google/api/annotations.proto\nhttps://github.com/googleapis/googleapis/blob/master/google/api/http.proto\n\n// 拷贝到 protoc 编译器目录/include下\n/Users/nining/go/install/protoc-3.20.1-osx-aarch_64/include\nmkdir google/api\ncp annotations.proto http.proto google/api/\n```\n\n- 重新定义proto\n```js\nservice Goods {\n    // 获取直播间商品列表\n    rpc GetGoodsListByRoomId(GetGoodsListRoomReq) returns (GoodsListReply){\n        option (google.api.http) = {\n            post: \"/v1/goods\",\n            body: \"*\"\n        };\n    };\n    ...\n}\n```\n\n- 生成代码\n```sh\n\tprotoc  \\\n    --go_out=.  \\\n    --go_opt=paths=source_relative  \\\n    --go-grpc_out=. \\\n    --go-grpc_opt=paths=source_relative \\\n    --grpc-gateway_out=.    \\\n    --grpc-gateway_opt paths=source_relative    \\\n    ./proto/*.proto\n```\n- 服务端启动HTTP代理\n```go\n\t// gRPC-Gateway\n\tgo func() {\n\t\t// 创建一个连接到我们刚刚启动的 gRPC 服务器的客户端连接\n\t\t// gRPC-Gateway 就是通过它来代理请求（将HTTP请求转为RPC请求）\n\t\tconn, err := grpc.DialContext(\n\t\t\tcontext.Background(),\n\t\t\tfmt.Sprintf(\"%s:%d\", ip.String(), global.ServerSetting.GrpcPort),\n\t\t\tgrpc.WithBlock(),\n\t\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\t)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"grpc.DialContext err:\", err)\n\t\t}\n\t\tgwmux := runtime.NewServeMux()\n\t\t// 注册RegisterGoodsHandler\n\t\terr = pb.RegisterGoodsHandler(context.Background(), gwmux, conn)\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"Failed to register gateway:\", err)\n\t\t}\n\t\tgwServer := \u0026http.Server{\n\t\t\tAddr:    fmt.Sprintf(\":%d\", global.ServerSetting.HttpPort),\n\t\t\tHandler: gwmux,\n\t\t}\n\t\t// 提供gRPC-Gateway服务\n\t\tlog.Printf(\"Serving gRPC-Gateway on http://%s:%d\\n\", ip.String(), global.ServerSetting.HttpPort)\n\t\tlog.Fatalln(gwServer.ListenAndServe())\n\t}()\n```\n\n### Makefile 快速实现\n\n- [Makefile 快速入门](https://zhuanlan.zhihu.com/p/350297509)\n```js\n.PHONY: all build run gotool clean\n\nBINARY=\"charites_server\"\nPROTO_DIR=proto\n\nall: gotool build\n\nbuild:\n\tCGO_ENABLE=1 GOOS=darwin GOARCH=arm64 go build -o ${BINARY}\n\nrun:\n\tgo run main.go\n\ngotool:\n\tgo fmt ./\n\tgo vet ./\n\nclean:\n\t@if [ -f ${BINARY} ] ; then rm ${BINARY}; fi\n\ngen:\n\tprotoc  \\\n    --go_out=.  \\\n    --go_opt=paths=source_relative  \\\n    --go-grpc_out=. \\\n    --go-grpc_opt=paths=source_relative \\\n    --grpc-gateway_out=.    \\\n    --grpc-gateway_opt paths=source_relative    \\\n    $(shell find $(PROTO_DIR) -iname \"*.proto\")\n\nhello:\n\tgo run  client/helloworld.go\n\nhelp:\n\t@echo \"make build - 编译指定文件\"\n\t@echo \"make run - 直接运行项目\"\n\t@echo \"make clean - 删除编译文件\"\n\t@echo \"make gen - 生成pb及grpc代码\"\n```\n\n\n## 5. 库存微服务\n\n实际项目中会把商品服务、库存服务、订单服务拆分为不同的微服务，我们仅作为测试项目，代码写到同一个项目中\n\n### 通用业务开发流程\n\n参考 商品功能开发 模块\n1. `storage/sql/stock.sql` 定义SQL语句\n2. `model/stock.go` 定义数据模型\n3. `proto/stock.proto` 生成gRPC代码结构\n4. `make gen` 生成代码\n5. `apps/stock/handler.go` 实现 `StockServer` 服务\n6. `main.go` 注册服务 `pb.RegisterStockServer(s, stock.NewStockServer())`\n7. `make run` 运行服务\n\n\n### 测试方法汇总\n```go\n// 1. 命令行工具 grpcurl\ngrpcurl -plaintext -rpc-header 'authorization:\"token\"' 192.168.1.4:8081 list\ngrpcurl -plaintext -rpc-header 'authorization:\"token\"'  -d '{\"GoodsId\": 100, \"Num\":1000}' 192.168.1.4:8081  Stock.GetStock\n// 2. 实现client端\ngo run client/stock.go\n// 3. gPRC-Gateway HTTP形式\n// 4. Swagger 文档\n```\n\n### 并发资源竞争示例\n- 服务端未加锁示例\n```go\n// apps/stock/dao.go\nfunc ReduceStock(ctx context.Context, goodsId, num int64) (*pb.GoodsStockInfo, error) {\n\tvar stock model.Stock\n\t// 1. 查询现有库存\n\tdb := global.DBEngine.WithContext(ctx).\n\t\tModel(\u0026model.Stock{}).\n\t\tWhere(\"id = ?\", goodsId).\n\t\tFirst(\u0026stock)\n\t// 不存在也会抛异常\n\tif db.Error != nil {\n\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", db.Error.Error()))\n\t\treturn nil, errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t}\n\tif db.RowsAffected == 0 {\n\t\treturn nil, errcode.ToRPCError(errcode.ErrorNotFoundStock)\n\t}\n\t// 2. 校验库存\n\tif stock.Num-num \u003c 0 {\n\t\treturn nil, errcode.ErrorNotEnoughStock\n\t}\n\t// 3. 扣减库存并保存\n\tstock.Num -= num\n\t// global.DBEngine.WithContext(ctx).Save(\u0026stock) // 更新所有字段\n\terr := global.DBEngine.WithContext(ctx).\n\t\tModel(\u0026model.Stock{}).\n\t\tWhere(\"id = ?\", goodsId).\n\t\tUpdates(map[string]interface{}{\n\t\t\t\"num\": stock.Num,\n\t\t}).Error\n\tif err != nil {\n\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", err.Error()))\n\t\treturn nil, errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t}\n\treturn \u0026pb.GoodsStockInfo{GoodsId: goodsId, Num: stock.Num}, nil\n}\n```\n- 客户端并发20请求\n```go\n// go run client/stock.go\nfunc main() {\n\t// 建立连接 with grpc.DialOption\n\tconn, err := grpc.Dial(\"consul://127.0.0.1:8500/shopping?healthy=true\",\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithUnaryInterceptor(middleware.ClientUnaryInterceptor),\n\t\tgrpc.WithDefaultServiceConfig(`{\"loadBalancingPolicy\": \"round_robin\"}`),\n\t)\n\t// 判断连接 err 与 defer 关闭连接\n\tif err != nil {\n\t\tlog.Fatalln(\"grpc.Dial err:\", err)\n\t}\n\tdefer conn.Close()\n\n\t// 获取操作gRPC服务端服务的client\n\tclient := pb.NewStockClient(conn)\n\n\t// 客户端业务逻辑处理，如并发20次操作服务端服务\n\tvar wg sync.WaitGroup\n\tfor i := 0; i \u003c 20; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tresp, err := client.ReduceStock(context.Background(), \u0026pb.GoodsStockInfo{GoodsId: 1, Num: 1})\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"client.ReduceStock Error: %v\\n\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfmt.Printf(\"resp GoodsId:%d, Num:%d\\n\", resp.GoodsId, resp.Num)\n\t\t}()\n\t}\n\twg.Wait()\n}\n```\n- 最终出产生资源竞争问题\n\n### 事务处理示例\n- 将多次数据库操作包装在一个事务中，实现要么成功要么失败，不会出现一个成功一个失败的情况\n- 批量扣减库存可以使用事务，事务解决不了资源竞争问题\n\n```go\nfunc ReduceStockWithTransaction(ctx context.Context, goodsId, num int64) (*pb.GoodsStockInfo, error) {\n\tvar stock model.Stock\n\terr := global.DBEngine.Transaction(func(tx *gorm.DB) error {\n\t\t// 1. 查询现有库存\n\t\tdb := tx.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tFirst(\u0026stock)\n\t\t// 不存在也会抛异常\n\t\tif db.Error != nil {\n\t\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", db.Error.Error()))\n\t\t\treturn errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\tif db.RowsAffected == 0 {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorNotFoundStock)\n\t\t}\n\t\t// 2. 校验库存\n\t\tif stock.Num-num \u003c 0 {\n\t\t\treturn errcode.ErrorNotEnoughStock\n\t\t}\n\t\t// 3. 扣减库存并保存\n\t\tstock.Num -= num\n\t\t// global.DBEngine.WithContext(ctx).Save(\u0026stock) // 更新所有字段\n\t\terr := tx.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tUpdates(map[string]interface{}{\n\t\t\t\t\"num\": stock.Num,\n\t\t\t}).Error\n\t\tif err != nil {\n\t\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", err.Error()))\n\t\t\treturn errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\t// return nil 提交事务，任何类型err回滚事务\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026pb.GoodsStockInfo{GoodsId: goodsId, Num: stock.Num}, nil\n}\n```\n\n### 悲观锁实现并发\n\n- [通俗易懂 锁入门](https://zhuanlan.zhihu.com/p/71156910)\n- [分布式锁看这篇就够了](https://zhuanlan.zhihu.com/p/42056183)\n- [MySQL 行级锁](https://www.cnblogs.com/zping/p/10955750.html)\n\n悲观锁，对一切事情比较悲观，我更新数据，就觉得所有人都要来跟我抢\n\n从查询数据的时候就给这条数据加锁，保证只有我能更新\n\n- 原生SQL\n```sql\nstart();\nselect * from t1 where goods_id = 1 for update;\nupdate t1 set num = 1 where goods_id = 1;\ncommit();\n```\n- 代码实现\n```go\n// 事务中添加 Clauses(clause.Locking{Strength: \"UPDATE\"}) 即可\nfunc ReduceStockWithPessimisticLock(ctx context.Context, goodsId, num int64) (*pb.GoodsStockInfo, error) {\n\tvar stock model.Stock\n\terr := global.DBEngine.Transaction(func(tx *gorm.DB) error {\n\t\t// 1. 查询现有库存\n\t\tdb := tx.WithContext(ctx).\n\t\t\tClauses(clause.Locking{Strength: \"UPDATE\"}).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tFirst(\u0026stock)\n\t\t// 不存在也会抛异常\n\t\tif db.Error != nil {\n\t\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", db.Error.Error()))\n\t\t\treturn errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\tif db.RowsAffected == 0 {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorNotFoundStock)\n\t\t}\n\t\t// 2. 校验库存\n\t\tif stock.Num-num \u003c 0 {\n\t\t\treturn errcode.ErrorNotEnoughStock\n\t\t}\n\t\t// 3. 扣减库存并保存\n\t\tstock.Num -= num\n\t\t// global.DBEngine.WithContext(ctx).Save(\u0026stock) // 更新所有字段\n\t\terr := tx.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tUpdates(map[string]interface{}{\n\t\t\t\t\"num\": stock.Num,\n\t\t\t}).Error\n\t\tif err != nil {\n\t\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", err.Error()))\n\t\t\treturn errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\t// return nil 提交事务，任何类型err回滚事务\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026pb.GoodsStockInfo{GoodsId: goodsId, Num: stock.Num}, nil\n}\n```\n- 注意事项\n1. 一定是基于索引来查询\n2. 放到事务中处理\n\n\n### 乐观锁实现并发\n和悲观锁一样都是宏观的一个概念，本质上不算锁\n\n乐观锁认为一般不会有人跟我竞争资源，通过version版本号在更新的时候做check\n\n- 原生SQL\n```sql\nselect goods_id,num,version from shopping_stock where goods_id = 1;\nupdate shopping_stock set num=1,version=version+1 where goods_id = 1 and verison=verison;\n```\n- 代码实现\n```go\nfunc ReduceStockWithOptimisticLock(ctx context.Context, goodsId, num int64) (*pb.GoodsStockInfo, error) {\n\tfor retry := 0; retry \u003c 20; retry++ {\n\t\tvar stock model.Stock\n\t\t// 1. 查询现有库存\n\t\tdb := global.DBEngine.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tFirst(\u0026stock)\n\t\t// 不存在也会抛异常\n\t\tif db.Error != nil {\n\t\t\tglobal.Logger.Error(\"model.Stock.First\", zap.String(\"error\", db.Error.Error()))\n\t\t\treturn nil, errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\tif db.RowsAffected == 0 {\n\t\t\treturn nil, errcode.ToRPCError(errcode.ErrorNotFoundStock)\n\t\t}\n\t\t// 2. 校验库存\n\t\tif stock.Num-num \u003c 0 {\n\t\t\treturn nil, errcode.ErrorNotEnoughStock\n\t\t}\n\t\t// 3. 扣减库存并保存\n\t\tret := global.DBEngine.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ? and version = ?\", goodsId, stock.Version).\n\t\t\tUpdates(map[string]interface{}{\n\t\t\t\t\"num\":     stock.Num - 1,\n\t\t\t\t\"version\": stock.Version + 1,\n\t\t\t})\n\t\tif ret.Error != nil {\n\t\t\tglobal.Logger.Error(\"model.Stock.Updates\", zap.String(\"error\", ret.Error.Error()))\n\t\t\treturn nil, errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\tif ret.RowsAffected == 0 {\n\t\t\t// 说明 version 被更新，重试即可\n\t\t\tcontinue\n\t\t}\n\t\treturn \u0026pb.GoodsStockInfo{GoodsId: goodsId, Num: stock.Num}, nil\n\t}\n\treturn nil, errcode.ToRPCError(errcode.ErrorNeedRetryStock)\n}\n```\n- 注意事项\n1. `var stock model.Stock` 定义在 for 循环里面\n2. `continue` 重试逻辑判断点\n\n\n### 分布式锁实现并发\n\n借助其他的组件：redis、zookeeper、etcd\n\n基于redis实现：[https://github.com/go-redsync/redsync](https://github.com/go-redsync/redsync)\n\n原生redis实现：setnx [https://www.redis.net.cn/order/3552.html](https://www.redis.net.cn/order/3552.html)\n\n完善的基于redis的分布式锁：redlock [https://zhuanlan.zhihu.com/p/62769627](https://zhuanlan.zhihu.com/p/62769627)\n\n- 代码实现\n```go\nfunc ReduceStockWithDistributedLock(ctx context.Context, goodsId, num int64) (*pb.GoodsStockInfo, error) {\n\tmutexname := fmt.Sprintf(\"reduce:stock:mutex:%d\", goodsId)\n\tmutex := global.Redsync.NewMutex(mutexname)\n\tif err := mutex.Lock(); err != nil {\n\t\treturn nil, errcode.ToRPCError(errcode.ErrorRedisLockStock)\n\t}\n\tdefer mutex.Unlock()\n\n\tvar stock model.Stock\n\terr := global.DBEngine.Transaction(func(tx *gorm.DB) error {\n\t\t// 1. 查询现有库存\n\t\tdb := tx.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tFirst(\u0026stock)\n\t\t// 不存在也会抛异常\n\t\tif db.Error != nil {\n\t\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", db.Error.Error()))\n\t\t\treturn errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\tif db.RowsAffected == 0 {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorNotFoundStock)\n\t\t}\n\t\t// 2. 校验库存\n\t\tif stock.Num-num \u003c 0 {\n\t\t\treturn errcode.ErrorNotEnoughStock\n\t\t}\n\t\t// 3. 扣减库存并保存\n\t\tstock.Num -= num\n\t\t// global.DBEngine.WithContext(ctx).Save(\u0026stock) // 更新所有字段\n\t\terr := tx.WithContext(ctx).\n\t\t\tModel(\u0026model.Stock{}).\n\t\t\tWhere(\"id = ?\", goodsId).\n\t\t\tUpdates(map[string]interface{}{\n\t\t\t\t\"num\": stock.Num,\n\t\t\t}).Error\n\t\tif err != nil {\n\t\t\tglobal.Logger.Error(\"ErrorDBOperateStock\", zap.String(\"error\", err.Error()))\n\t\t\treturn errcode.ToRPCError(errcode.ErrorDBOperateStock)\n\t\t}\n\t\t// return nil 提交事务，任何类型err回滚事务\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026pb.GoodsStockInfo{GoodsId: goodsId, Num: stock.Num}, nil\n}\n```\n\n小结：\n- 悲观锁阻塞事务 乐观锁回滚重试\n- 乐观锁，本质上不加锁，适用于写操作少的场景\n\n\n## 6. 订单微服务\n\n\n### 微服务相互调用\n\n- 修改`Makefile`，启用不同端口号实例微服务\n```js\n// 商品微服务\nrun_goods:\n\tgo run main.go -p 8090\n\n// 库存微服务\nrun_stock:\n\tgo run main.go -p 8092\n```\n\n- 初始化微服务客户端\n```go\n// bootstrap/rpc.go\nfunc setupRPClient() error {\n\t// 商品微服务客户端\n\tgoodsConn, err := grpc.Dial(\"127.0.0.1:8090\",\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithUnaryInterceptor(middleware.ClientUnaryInterceptor),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tglobal.GoodsCli = pb.NewGoodsClient(goodsConn)\n\n\t// 库存微服务客户端\n\tstockConn, err := grpc.Dial(\"127.0.0.1:8092\",\n\t\tgrpc.WithTransportCredentials(insecure.NewCredentials()),\n\t\tgrpc.WithUnaryInterceptor(middleware.ClientUnaryInterceptor),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\tglobal.StockCli = pb.NewStockClient(stockConn)\n\treturn nil\n}\n```\n\n实现订单微服务，直接通过全局客户端调用其他微服务接口\n```go\nresp, err := global.GoodsCli.GetGoodsDetail(context.Background(), \u0026pb.GetGoodsDetailReq{GoodsId: 1})\nresp, err := global.StockCli.ReduceStock(context.Background(), \u0026pb.GoodsStockInfo{GoodsId: 1, Num: 1})\n```\n\n### 雪花算法订单号\n\n- [分布式ID神器之雪花算法简介](https://zhuanlan.zhihu.com/p/85837641)\n- [雪花算法go实现](https://github.com/bwmarrin/snowflake)\n\n分布式服务，需要把雪花算法当成一个独立的服务部署\n\n```go\nimport (\n\t\"charites/global\"\n\t\"errors\"\n\t\"time\"\n\n\tsf \"github.com/bwmarrin/snowflake\"\n)\n// global/snowflake.go\nvar SnowNode *sf.Node\n\n// bootstrap/snowflake.go\nconst (\n\t_defaultStartTime = \"2021-12-31\"\n)\n\nfunc setupSnowflake(startTime string, machineId int64) error {\n\tif machineId \u003c 0 {\n\t\treturn errors.New(\"snowflake need machineId\")\n\t}\n\tif len(startTime) == 0 {\n\t\tstartTime = _defaultStartTime\n\t}\n\tvar st time.Time\n\tst, err := time.Parse(\"2006-01-02\", startTime)\n\tif err != nil {\n\t\treturn err\n\t}\n\tsf.Epoch = st.UnixNano() / 100_0000          // 时间戳，开始时间 69年\n\tglobal.SnowNode, err = sf.NewNode(machineId) // 机器编号，1024\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nerr = setupSnowflake(\"\", 1)\nif err != nil {\n    log.Fatalf(\"init.setupSnowflake err: %v\", err)\n}\n\n// pkg/utils.go\nfunc GenId() int64 {\n\t// 坑：前端展示不了 int64，需要String()\n\treturn global.SnowNode.Generate().Int64()\n}\n```\n\n### 创建订单直接版\n```go\n// CreateOrder 创建订单\nfunc CreateOrder(ctx context.Context, req *pb.OrderReq) (*emptypb.Empty, error) {\n\t// 生成订单号\n\torderId := utils.GenInt64()\n\n\t// 请求商品微服务\n\tgoodsDetail, err := global.GoodsCli.GetGoodsDetail(context.Background(), \u0026pb.GetGoodsDetailReq{GoodsId: req.GoodsId})\n\tif err != nil {\n\t\treturn nil, errcode.ToRPCError(errcode.ErrorRPCOrderToGoods)\n\t}\n\t// 拿到商品价格作为支付价格\n\tprice, _ := strconv.ParseInt(goodsDetail.Price, 10, 64)\n\n\t// 请求库存微服务，扣减库存\n\t_, err = global.StockCli.ReduceStock(context.Background(), \u0026pb.GoodsStockInfo{GoodsId: req.GoodsId, Num: req.Num})\n\tif err != nil {\n\t\treturn nil, errcode.ToRPCError(errcode.ErrorRPCOrderToGoods)\n\t}\n\n\t// 创建订单与订单详情\n\torderData := model.Order{\n\t\tUserId:         req.UserId,\n\t\tOrderId:        orderId, // 雪花算法生成\n\t\tTradeId:        fmt.Sprintf(\"%d\", orderId),\n\t\tStatus:         int64(100), // 创建订单初始状态\n\t\tReceiveAddress: req.Address,\n\t\tReceiveName:    req.Name,\n\t\tReceivePhone:   req.Phone,\n\t\tPayAmount:      price * req.Num, // 该订单总价\n\t}\n\n\tmarketPrice, _ := strconv.ParseInt(goodsDetail.MarketPrice, 10, 64)\n\torderDetail := model.OrderDetail{\n\t\tUserId:    req.UserId,\n\t\tOrderId:   orderId, // 雪花算法生成\n\t\tGoodsId:   req.GoodsId,\n\t\tNum:       req.Num,\n\t\tPayAmount: price * req.Num, // 该商品总价\n\n\t\tTitle:       goodsDetail.Title,\n\t\tMarketPrice: marketPrice,\n\t\tPrice:       price,\n\t\tBrief:       goodsDetail.Brief,\n\t\tHeadImgs:    strings.Join(goodsDetail.HeadImgs, \",\"),\n\t\tVideos:      strings.Join(goodsDetail.Videos, \",\"),\n\t\tDetail:      strings.Join(goodsDetail.Detail, \",\"),\n\t}\n\terr = global.DBEngine.Transaction(func(tx *gorm.DB) error {\n\t\torderResult := tx.WithContext(ctx).Create(\u0026orderData)\n\t\tif orderResult.Error != nil {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorCreateOrder)\n\t\t}\n\t\torderDetailResult := tx.WithContext(ctx).Create(\u0026orderDetail)\n\t\tif orderDetailResult.Error != nil {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorCreateOrderDetal)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn \u0026emptypb.Empty{}, nil\n}\n```\n\n- 测试创建订单\n```go\ngrpcurl \\\n-plaintext  \\\n-rpc-header 'authorization:\"token\"'  \\\n-d '{\"GoodsId\": 1, \"Num\": 2, \"UserId\": 1, \"Address\":\"BJ\", \"Name\":\"linda\", \"Phone\":\"18210980038\"}' \\\n192.168.1.4:8081  \\\nOrder.CreateOrder\n```\n\n- 存在的问题\n当扣减库存成功，但本地创建订单失败时，会导致数据不一致\n\n\n## 7. 分布式事务\n\n### 分布式事务介绍\n\n微服务架构下带来的挑战：怎么解决分布式场景下数据一致性问题，分布式事务\n\n- 讨论的前提：理论依据\n```js\n本地事务、分布式事务\n强一致性、弱一致性、最终一致性\nCAP理论：C一致性 A可用性 P分区容错性\nBASE理论：面向的是大型高可用可扩展的分布式系统，和传统的事物ACID特性是相反的，它完全不同于ACID的强一致性模型，而是通过牺牲强一致性来获得可用性，并允许数据在一段时间内是不一致的，但最终达到一致状态\n柔性事务\n可见性(对外可查询)，全局唯一的标识用于查询\n幂等操作，方便重试\n```\n\n\n- 常见分布式事务实现方式\n```js\n\n\n最大努力通知\n本质：通过定期校对，实现数据一致性\n- 支付宝/微信支付 通过回调的方式通知业务方支付状态\n- callback --\u003e 1 3 5 10 15 30 60\n- 提供一个查询接口，业务方主动去查询\n场景：适用于对业务最终一致性的时间敏感度低的系统\n```\n\n\nhttps://github.com/dtm-labs/dtm/blob/main/helper/README-cn.md\n\n\n- [看一遍就理解：分布式事务详解](https://zhuanlan.zhihu.com/p/516554367)\n\n\n\n\n\n\n\n## 8. RocketMQ入门\n\n- 官文文档 [https://github.com/apache/rocketmq/tree/master/docs/cn](https://github.com/apache/rocketmq/tree/master/docs/cn)\n\n### 本地安装RocketMQ\n\n- 推荐使用docker-compose 快速搭建本地开发环境\n- [https://github.com/foxiswho/docker-rocketmq](https://github.com/foxiswho/docker-rocketmq)\n\n```go\ngit clone  https://github.com/foxiswho/docker-rocketmq.git\ncd docker-rocketmq\ncd rmq\n```\n- 修改一下`docker-compose.yml`文件，暂时使用 阿里云 镜像库里的4.7.0版本\n```go\nversion: '3.5'\n\nservices:\n  rmqnamesrv:\n#    image: foxiswho/rocketmq:4.9.2\n    image: registry.cn-hangzhou.aliyuncs.com/foxiswho/rocketmq:4.7.0\n    container_name: rmqnamesrv\n    ports:\n      - 9876:9876\n    volumes:\n      - ./rmqs/logs:/home/rocketmq/logs\n      - ./rmqs/store:/home/rocketmq/store\n    environment:\n      JAVA_OPT_EXT: \"-Duser.home=/home/rocketmq -Xms512M -Xmx512M -Xmn128m\"\n    command: [\"sh\",\"mqnamesrv\"]\n    networks:\n        rmq:\n          aliases:\n            - rmqnamesrv\n  rmqbroker:\n#    image: foxiswho/rocketmq:4.9.2\n    image: registry.cn-hangzhou.aliyuncs.com/foxiswho/rocketmq:4.7.0\n    container_name: rmqbroker\n    ports:\n      - 10909:10909\n      - 10911:10911\n    volumes:\n      - ./rmq/logs:/home/rocketmq/logs\n      - ./rmq/store:/home/rocketmq/store\n      - ./rmq/brokerconf/broker.conf:/etc/rocketmq/broker.conf\n    environment:\n        JAVA_OPT_EXT: \"-Duser.home=/home/rocketmq -Xms512M -Xmx512M -Xmn128m\"\n    command: [\"sh\",\"mqbroker\",\"-c\",\"/etc/rocketmq/broker.conf\",\"-n\",\"rmqnamesrv:9876\",\"autoCreateTopicEnable=true\"]\n    depends_on:\n      - rmqnamesrv\n    networks:\n      rmq:\n        aliases:\n          - rmqbroker\n\n  rmqconsole:\n    image: styletang/rocketmq-console-ng\n    container_name: rmqconsole\n    ports:\n      - 8180:8080\n    environment:\n        JAVA_OPTS: \"-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false\"\n    depends_on:\n      - rmqnamesrv\n    networks:\n      rmq:\n        aliases:\n          - rmqconsole\n\nnetworks:\n  rmq:\n    name: rmq\n    driver: bridge\n```\n- 修改配置文件 `vim rmq/brokerconf/broker.conf`\n\n```go\n// 将33行取消注释，并将 `brokerIP1` 为你本机的IP地址\nbrokerIP1=192.168.1.4\n```\n- 执行本地安装\n```go\nchmod +x  start.sh\n./start.sh\n\n// 本地访问\nhttp://localhost:8180\n```\n![](./assets/rmq.png)\n\n\n### Go语言客户端\n\n- [https://github.com/apache/rocketmq-client-go](https://github.com/apache/rocketmq-client-go)\n\n- [https://github.com/apache/rocketmq-client-go/tree/master/examples](https://github.com/apache/rocketmq-client-go/tree/master/examples)\n\n\n## 9. 分布式订单\n\n- 基于RocketMQ事务消息实现订单微服务的分布式事务\n- 逆向思路：先尝试返送回滚库存消息\n\t* 本地事务成功，撤销 滚库存消息\n\t* 本地事务失败，确认 滚库存消息\n\n### 本地事务订单逻辑\n- 按照 RocktMQ 事务消息实现两个方法 `ExecuteLocalTransaction`和 `CheckLocalTransaction`\n```go\n// OrderListener 自定义结构体，实现两个方法\n// 发送事务消息的时候，RocketMQ会根据情况自动调用这两个方法\ntype OrderListener struct {\n\tOrderId int64\n\tParam   *pb.OrderReq\n\tErr     error\n}\n\n// 当发送prepare(half) message 成功后，这个方法(执行本地事务)就会被执行\nfunc (o *OrderListener) ExecuteLocalTransaction(*primitive.Message) primitive.LocalTransactionState {\n\tif o.Param == nil {\n\t\tglobal.Logger.Error(\"ExecuteLocalTransaction param is nil\")\n\t\to.Err = errcode.ToRPCError(errcode.ErrorOrderEntityParam)\n\t\t// 库存未扣减\n\t\treturn primitive.RollbackMessageState\n\t}\n\tparam := o.Param\n\tctx := context.Background()\n\n\t// 请求商品微服务，查询商品金额(营销相关)\n\tgoodsDetail, err := global.GoodsCli.GetGoodsDetail(ctx, \u0026pb.GetGoodsDetailReq{GoodsId: param.GoodsId})\n\tif err != nil {\n\t\tglobal.Logger.Error(\"GoodsCli.GetGoodsDetail failed\", zap.Error(err))\n\t\to.Err = errcode.ToRPCError(errcode.ErrorRPCOrderToGoods)\n\t\t// 库存未扣减\n\t\treturn primitive.RollbackMessageState\n\t}\n\t// 拿到商品价格作为支付价格\n\tprice, _ := strconv.ParseInt(goodsDetail.Price, 10, 64)\n\n\t// 请求库存微服务，扣减库存\n\t_, err = global.StockCli.ReduceStock(ctx, \u0026pb.GoodsStockInfo{GoodsId: param.GoodsId, Num: param.Num, OrderId: o.OrderId})\n\tif err != nil {\n\t\tglobal.Logger.Error(\"StockCli.ReduceStock failed\", zap.Error(err))\n\t\to.Err = errcode.ToRPCError(errcode.ErrorRPCOrderToGoods)\n\t\t// 库存未扣减\n\t\treturn primitive.RollbackMessageState\n\t}\n\n\t// 本地事务创建订单与订单详情\n\torderData := model.Order{\n\t\tUserId:         param.UserId,\n\t\tOrderId:        o.OrderId, // 雪花算法生成\n\t\tTradeId:        fmt.Sprintf(\"%d\", o.OrderId),\n\t\tStatus:         int64(100), // 创建订单初始状态\n\t\tReceiveAddress: param.Address,\n\t\tReceiveName:    param.Name,\n\t\tReceivePhone:   param.Phone,\n\t\tPayAmount:      price * param.Num, // 该订单总价\n\t}\n\tmarketPrice, _ := strconv.ParseInt(goodsDetail.MarketPrice, 10, 64)\n\torderDetail := model.OrderDetail{\n\t\tUserId:      param.UserId,\n\t\tOrderId:     o.OrderId, // 雪花算法生成\n\t\tGoodsId:     param.GoodsId,\n\t\tNum:         param.Num,\n\t\tPayAmount:   price * param.Num, // 该商品总价\n\t\tTitle:       goodsDetail.Title,\n\t\tMarketPrice: marketPrice,\n\t\tPrice:       price,\n\t\tBrief:       goodsDetail.Brief,\n\t\tHeadImgs:    strings.Join(goodsDetail.HeadImgs, \",\"),\n\t\tVideos:      strings.Join(goodsDetail.Videos, \",\"),\n\t\tDetail:      strings.Join(goodsDetail.Detail, \",\"),\n\t}\n\terr = global.DBEngine.Transaction(func(tx *gorm.DB) error {\n\t\torderResult := tx.WithContext(ctx).Create(\u0026orderData)\n\t\tif orderResult.Error != nil {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorCreateOrder)\n\t\t}\n\t\torderDetailResult := tx.WithContext(ctx).Create(\u0026orderDetail)\n\t\tif orderDetailResult.Error != nil {\n\t\t\treturn errcode.ToRPCError(errcode.ErrorCreateOrderDetal)\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\t// 本地事务执行失败，但上一步库存已经扣减成功\n\t\treturn primitive.CommitMessageState\n\t}\n\n\t// 发送延迟消息\n\t// 不同等级：1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h\n\t// 消息中具体的载荷，定义为一个结构体，赞\n\tdata := model.OrderGoodsStockInfo{\n\t\tOrderId: o.OrderId,\n\t\tGoodsId: param.GoodsId,\n\t\tNum:     param.Num,\n\t}\n\tb, _ := json.Marshal(data)\n\t// 定义RocketMQ消息体\n\tmsg := primitive.NewMessage(global.RocketMQSetting.TopicOrderPayTimeout, b)\n\tmsg.WithDelayTimeLevel(3)\n\t_, err = global.Producer.SendSync(context.Background(), msg)\n\tif err != nil {\n\t\t// 延时消息发送失败\n\t\tglobal.Logger.Error(\"send delay msg failed\", zap.Error(err))\n\t\treturn primitive.CommitMessageState\n\t}\n\t// 说明本地事务执行成功，不需要发送回滚库存的消息\n\treturn primitive.RollbackMessageState\n}\n\n// 当发送prepare(half) message 没有响应时，broker会回查本地事务状态，此时这个方法被执行\nfunc (o *OrderListener) CheckLocalTransaction(*primitive.MessageExt) primitive.LocalTransactionState {\n\t// 检查本地是否订单创建成功即可\n\tvar count int64\n\tglobal.DBEngine.\n\t\tWithContext(context.Background()).\n\t\tModel(\u0026model.Order{}).Where(\"order_id = ?\", o.OrderId).\n\t\tCount(\u0026count)\n\tif count \u003c= 0 {\n\t\t// 说明订单创建失败，需要回滚库存\n\t\treturn primitive.CommitMessageState\n\t}\n\t// 不存回滚库存\n\treturn primitive.RollbackMessageState\n}\n```\n\n### 库存微服务回滚\n\n不能简单的收到回滚库存消息就回滚库存，因为有可能消息重复了，导致多次回滚，数据不一致的问题\n\n- 库存微服务启动消息监听\n```go\nfunc StartStockConsume() {\n\t// 库存微服务启动消息监听\n\tc, _ := rocketmq.NewPushConsumer(\n\t\tconsumer.WithNsResolver(primitive.NewPassthroughResolver([]string{global.RocketMQSetting.NameServer})),\n\t\tconsumer.WithGroupName(global.RocketMQSetting.GroupStockService),\n\t)\n\t// 监听Topck\n\terr := c.Subscribe(global.RocketMQSetting.TopicStockRollback, consumer.MessageSelector{}, stock.RollbackMsgHandle)\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t}\n\terr = c.Start()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\n// RollbackMsgHandle 监听RocketMQ消息进行库存回滚的处理函数\n// 需要考虑重复归还的问题(幂等性) --\u003e 添加库存扣减记录\nfunc RollbackMsgHandle(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {\n\tfor i := range msgs {\n\t\tvar data model.OrderGoodsStockInfo\n\t\terr := json.Unmarshal(msgs[i].Body, \u0026data)\n\t\tif err != nil {\n\t\t\tglobal.Logger.Error(\"json.Unmarshal RollbackMsg error\", zap.Error(err))\n\t\t\tcontinue\n\t\t}\n\t\terr = RollbackStockByMsg(ctx, \u0026data)\n\t\tif err != nil {\n\t\t\treturn consumer.ConsumeRetryLater, nil\n\t\t}\n\t\treturn consumer.ConsumeSuccess, nil\n\t}\n\treturn consumer.ConsumeSuccess, nil\n}\n```\n\n### 订单未支付延时消息\n\n1. 什么时机发送延迟消息？\n   1. 创建的订单时候 --\u003e 发延迟消息 --\u003e30分钟\n2. 发送方是谁？接收方又是谁？\n   1. 订单服务发送\n   2. 库存作为接收方的问题 --\u003e 收到这个延迟消息就要回滚库存吗？\n      1.  并不是，我们需要根据订单的状态去判断是否执行库存回滚\n   3. 我们仍然选择在订单服务接收延时消息\n      1. 收到消息就可以直接判断订单状态，\n      2. 如果是**未支付状态**就发送一条回滚库存的消息给库存服务，复用上一步的`shopping_stock_rollback`这个topic\n\n- 订单微服务监听超时消息\n```go\nfunc StartOrderConsume() {\n\t// 订单微服务监听超时消息\n\tc, _ := rocketmq.NewPushConsumer(\n\t\tconsumer.WithNsResolver(primitive.NewPassthroughResolver([]string{global.RocketMQSetting.NameServer})),\n\t\tconsumer.WithGroupName(global.RocketMQSetting.GroupOrderService),\n\t)\n\t// 订阅topic\n\terr := c.Subscribe(global.RocketMQSetting.TopicOrderPayTimeout, consumer.MessageSelector{}, order.OrderTimeoutHandle)\n\tif err != nil {\n\t\tfmt.Println(err.Error())\n\t}\n\terr = c.Start()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n```\n\n## 10. 本地部署启动\n\n```go\n// 启动商品微服务\nmake run_goods\n\n// 启动库存微服务\nmake run_stock\n\n// 启动订单微服务\nmake run\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fni-ning%2Fcharites","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fni-ning%2Fcharites","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fni-ning%2Fcharites/lists"}