• 22、库存服务


    一、库存服务的重要性

    在这里插入图片描述

    二、库存表结构与proto接口

    • 数据库创建:mxshop_inventory_srv
      在这里插入图片描述
    • inventory_srv/model/inventory.go:表结构
    package model
    
    type Inventory struct {
    	BaseModel
    	Goods   int32 `gorm:"type:int;index"` // 商品id
    	Stocks  int32 `gorm:"type:int"`       // 库存
    	Version int32 `gorm:"type:int"`       //分布式锁的乐观锁
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • inventory_srv/model/main/main.go:gorm建表
    package main
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"gorm.io/gorm/schema"
    	"log"
    	"nd/inventory_srv/global"
    	"nd/inventory_srv/initialize"
    	"nd/inventory_srv/model"
    	"os"
    	"time"
    )
    
    func main() {
    	initialize.InitConfig()
    	dsn := fmt.Sprintf("root:jiushi@tcp(%s:3306)/mxshop_inventory_srv?charset=utf8mb4&parseTime=True&loc=Local", global.ServerConfig.MysqlInfo.Host)
    
    	newLogger := logger.New(
    		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
    		logger.Config{
    			SlowThreshold: time.Second, // 慢 SQL 阈值
    			LogLevel:      logger.Info, // Log level
    			Colorful:      true,        // 禁用彩色打印
    		},
    	)
    
    	// 全局模式
    	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    		NamingStrategy: schema.NamingStrategy{
    			SingularTable: true,
    		},
    		Logger: newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    
    	_ = db.AutoMigrate(&model.Inventory{})
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • inventory_srv/proto/inventory.protoprotoc --go_out=. --go_opt=paths=import --go-grpc_out=. --go-grpc_opt=paths=import *.proto
    syntax = "proto3";
    import "google/protobuf/empty.proto";
    option go_package = ".;proto";
    
    
    service Inventory {
      rpc SetInv(GoodsInvInfo) returns(google.protobuf.Empty); //设置库存
      rpc InvDetail(GoodsInvInfo) returns (GoodsInvInfo); // 获取库存信息
      // 购买的时候,有可能是从购物车购买的,这就可能涉及到多件商品的购买库存;这里还涉及到了分布式事务
      rpc Sell(SellInfo) returns (google.protobuf.Empty); //库存扣减
      rpc Reback(SellInfo) returns(google.protobuf.Empty); //库存归还
    }
    
    message GoodsInvInfo {
      int32 goodsId = 1;
      int32 num = 2;
    }
    
    message SellInfo {
      repeated GoodsInvInfo goodsInfo = 1;
      string orderSn = 2;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    三、快速拉起inventory服务

    • inventory_srv/main.go:proto注册
    	server := grpc.NewServer()
    	proto.RegisterInventoryServer(server, &proto.UnimplementedInventoryServer{})
    	lis, err := net.Listen("tcp", fmt.Sprintf("%s:%d", *IP, *Port))
    	if err != nil {
    		panic("failed to listen:" + err.Error())
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • nacos新建命名空间
      在这里插入图片描述
      在这里插入图片描述
    {
      "name": "inventory_srv",
      "host": "192.168.78.1",
      "tags": ["imooc", "bobby", "inventory", "srv"],
      "mysql": {
        "host": "192.168.78.131",
        "port": 3306,
        "user": "root",
        "password": "jiushi",
        "db": "mxshop_inventory_srv"
      },
      "consul": {
        "host": "192.168.78.131",
        "port": 8500
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 修改yaml的命名空间为inventory的命名空间id
    host: '192.168.78.131'
    port: 8848
    namespace: '2a8c0128-127b-4356-8670-811eb688f7bd'
    user: 'nacos'
    password: 'nacos'
    dataid: 'inventory_srv.json'
    group: 'comp'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述


    四、库存服务接口实现

    1 - 设置库存接口

    • inventory_srv/handler/inventory.go
    func (*InventoryServer) SetInv(ctx context.Context, req *proto.GoodsInvInfo) (*emptypb.Empty, error) {
    	//设置库存, 如果我要更新库存
    	var inv model.Inventory
    	global.DB.Where(&model.Inventory{Goods: req.GoodsId}).First(&inv)
    	inv.Goods = req.GoodsId
    	inv.Stocks = req.Num
    
    	global.DB.Save(&inv)
    	return &emptypb.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2 - 获取库存接口

    • inventory_srv/handler/inventory.go
    func (*InventoryServer) InvDetail(ctx context.Context, req *proto.GoodsInvInfo) (*proto.GoodsInvInfo, error) {
    	var inv model.Inventory
    	if result := global.DB.Where(&model.Inventory{Goods: req.GoodsId}).First(&inv); result.RowsAffected == 0 {
    		return nil, status.Errorf(codes.NotFound, "没有库存信息")
    	}
    	return &proto.GoodsInvInfo{
    		GoodsId: inv.Goods,
    		Num:     inv.Stocks,
    	}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    3 - 扣减库存(本地事务)

    func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    	// 扣减库存,本地事务
    	// 数据库基本的一个应用场景:数据库事务
    	// 并发情况之下 可能会出现超卖 1
    	tx := global.DB.Begin()
    	for _, goodInfo := range req.GoodsInfo {
    		var inv model.Inventory
    		if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
    			tx.Rollback() // 回滚之前的操作
    			return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
    		}
    		// 判断库存是否充足
    		if inv.Stocks < goodInfo.Num {
    			tx.Rollback() // 回滚之前的操作
    			return nil, status.Errorf(codes.ResourceExhausted, "库存不足")
    		}
    		// 扣减,这里会出现数据不一致的问题
    		inv.Stocks -= goodInfo.Num
    		tx.Save(&inv) // 一旦使用了事务的,保存修改数据库的操作就需要使用事务的tx,而不能使用db
    	}
    	tx.Commit() // 需要自己手动提交操作
    	return &emptypb.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    4 - 库存归还(本地事务)

    func (*InventoryServer) Reback(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    	//库存归还: 1:订单超时归还 2. 订单创建失败,归还之前扣减的库存 3. 手动归还
    	tx := global.DB.Begin()
    	for _, goodInfo := range req.GoodsInfo {
    		var inv model.Inventory
    		if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
    			tx.Rollback() //回滚之前的操作
    			return nil, status.Errorf(codes.InvalidArgument, "没有库存信息")
    		}
    
    		//扣减, 会出现数据不一致的问题 - 锁,分布式锁
    		inv.Stocks += goodInfo.Num
    		tx.Save(&inv)
    	}
    	tx.Commit() // 需要自己手动提交操作
    	return &emptypb.Empty{}, nil
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5 - 接口测试

    • inventory_srv/main.go:修改端口为50059;proto注册对象修改为&handler.InventoryServer{}
    func main() {
    	IP := flag.String("ip", "0.0.0.0", "ip地址")
    	Port := flag.Int("port", 50059, "端口号") // 这个修改为0,如果我们从命令行带参数启动的话就不会为0
    
    	//初始化
    	initialize.InitLogger()
    	initialize.InitConfig()
    	initialize.InitDB()
    	zap.S().Info(global.ServerConfig)
    
    	flag.Parse()
    	zap.S().Info("ip: ", *IP)
    	if *Port == 0 {
    		*Port, _ = utils.GetFreePort()
    	}
    	zap.S().Info("port: ", *Port)
    
    	server := grpc.NewServer()
    	proto.RegisterInventoryServer(server, &handler.InventoryServer{})
    	// 省略。。。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • inventory_srv/tests/test_config.go
    package tests
    
    var (
    	TargetAddr = "127.0.0.1:50059"
    )
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 测试前提
      • goods_srv服务启动:端口50058
      • inventory_srv服务启动:端口50059
      • 运行 inventory_srv/tests/inventory/main.go
    package main
    
    import (
    	"context"
    	"fmt"
    	"google.golang.org/grpc"
    	"google.golang.org/grpc/credentials/insecure"
    
    	"nd/inventory_srv/proto"
    	"nd/inventory_srv/tests"
    )
    
    var invClient proto.InventoryClient
    var conn *grpc.ClientConn
    
    func Init() {
    	var err error
    	conn, err = grpc.Dial(tests.TargetAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    	if err != nil {
    		panic(err)
    	}
    	invClient = proto.NewInventoryClient(conn)
    }
    
    func TestSetInv(goodsId, Num int32) {
    	_, err := invClient.SetInv(context.Background(), &proto.GoodsInvInfo{
    		GoodsId: goodsId,
    		Num:     Num,
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println("设置库存成功")
    }
    
    func TestInvDetail(goodsId int32) {
    	rsp, err := invClient.InvDetail(context.Background(), &proto.GoodsInvInfo{
    		GoodsId: goodsId,
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println(rsp.Num)
    }
    
    func TestSell() {
    	/*
    		1. 第一件扣减成功: 第二件: 1. 没有库存信息 2. 库存不足
    		2. 两件都扣减成功
    	*/
    	_, err := invClient.Sell(context.Background(), &proto.SellInfo{
    		GoodsInfo: []*proto.GoodsInvInfo{
    			{GoodsId: 1, Num: 1},
    			{GoodsId: 2, Num: 70},
    		},
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println("库存扣减成功")
    }
    
    func TestReback() {
    	_, err := invClient.Reback(context.Background(), &proto.SellInfo{
    		GoodsInfo: []*proto.GoodsInvInfo{
    			{GoodsId: 1, Num: 10},
    			{GoodsId: 100, Num: 30},
    		},
    	})
    	if err != nil {
    		panic(err)
    	}
    	fmt.Println("归还成功")
    }
    
    func main() {
    	Init()
    	//var i int32
    	//for i = 1; i <= 9; i++ {
    	//	TestSetInv(i, 90)
    	//}
    
    	//TestInvDetail(2)
    	//TestSell()
    	TestReback()
    	conn.Close()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88

    五、完整源码

    • 完整源码下载mxshop_srvsV8.6.rar
    • 源码说明:(nacos的ip配置自行修改,全局变量DEV_CONFIG设置:1=zsz,2=comp,3=home)
      • goods_srv/model/sql/mxshop_goods.sql:包含了建表语句
      • other_import/api.json:YApi的导入文件
      • other_import/nacos_config_export_user.zip:nacos的user配置集导入文件
      • other_import/nacos_config_export_goods.zip:nacos的goods配置集导入文件
      • other_import/nacos_config_export_inventory.zip:nacos的inventory的配置导入文件
  • 相关阅读:
    ChatGPT总结(持续更新)
    《痞子衡嵌入式半月刊》 第 31 期
    国产大模型参加高考,同写2024年高考作文,及格分(通义千问、Kimi、智谱清言、Gemini Advanced、Claude-3-Sonnet、GPT-4o)
    Postman抓包网页请求
    Postman-Installation has failed
    短信测压APP/网卡
    谷粒商城十二性能压测
    Cisco简单配置(八)—动态中继协议DTP
    神经网络建模的建模步骤,人工神经网络建模过程
    Servlet—servlet两种配置方式
  • 原文地址:https://blog.csdn.net/qq23001186/article/details/126265214