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"`       //分布式锁的乐观锁
}

  • 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{})
}

  • 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;
}

三、快速拉起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())
	}
  • 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
  }
}
  • 修改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 - 设置库存接口

  • 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
}

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
}

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
}

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
}

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{})
	// 省略。。。
  • inventory_srv/tests/test_config.go
package tests

var (
	TargetAddr = "127.0.0.1:50059"
)

  • 测试前提
    • 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()
}


五、完整源码

  • 完整源码下载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的配置导入文件
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无休止符

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值