10--RabbitMQ + 订单模块
当前抢购成功后,Redis 库存已扣减,但 MySQL 中没有真正生成订单(order表)。
需要通过消息队列异步创建订单,实现削峰填谷。
为此,我们先安装RabbitMQ依赖
go get -u github.com/rabbitmq/amqp091-go
注: 事先安装RabbitMQ
一. Config配置
与Redis、MySQL一样,想要接入RabbitMQ就需要我们修改Config
config/config.go
// RabbitMQConfig RabbitMQ配置
type RabbitMQConfig struct {
Host string `mapstructure:"host"` // RabbitMQ服务器地址
Port int `mapstructure:"port"` // 端口
User string `mapstructure:"user"` // 用户名
Password string `mapstructure:"password"` // 密码
VHost string `mapstructure:"vhost"` // 虚拟主机
}
// Config 整个配置树
type Config struct {
Server ServerConfig `mapstructure:"server"` // 服务器相关配置
Database DatabaseConfig `mapstructure:"database"` // 数据库相关配置
JWT JWTConfig `mapstructure:"jwt"` // JWT相关配置
Redis RedisConfig `mapstructure:"redis"` // Redis相关配置
RabbitMQ RabbitMQConfig `mapstructure:"rabbitmq"` // 新增 RabbitMQ相关配置
}
config/config.yaml
rabbitmq:
host: "127.0.0.1"
port: 5672 # 替换端口号
user: "guest" # 替换用户名
password: "guest" # 替换密码
vhost: "/"
二. MQ初始化
internal/mq/rabbitmq.go
package mq
import (
"fmt"
"github.com/Chuan81/secgo-mall/config"
amqp "github.com/rabbitmq/amqp091-go"
)
var Conn *amqp.Connection
var Channel *amqp.Channel
// OrderQueue 订单队列名称
const OrderQueue = "order_queue"
// InitRabbitMQ 初始化RabbitMQ连接和通道
func InitRabbitMQ() {
rCfg := config.GlobalConfig.RabbitMQ // 读取config文件
// 拼接AMQP连接URL
url := fmt.Sprintf(
"amqp://%s:%s@%s:%d/%s",
rCfg.User,
rCfg.Password,
rCfg.Host,
rCfg.Port,
rCfg.Vhost,
)
var err error
// 建立TCP连接
Conn, err = amqp.Dial(url)
if err != nil {
panic("RabbitMQ connection error:" + err.Error())
}
// 创建一个Channel(相当于socket连接,用于收发消息)
Channel, err = Conn.Channel()
if err != nil {
panic("RabbitMQ channel error:" + err.Error())
}
// 声明一个持久化队列(如不存在自动创建)
_, err = Channel.QueueDeclare(
OrderQueue,
true,
false,
false,
false,
nil,
)
if err != nil {
panic("RabbitMQ queue declare error:" + err.Error())
}
println("RabbitMQ connected successful")
}
三. 订单消息结构体
internal/api/handlers/dto/order.go
package dto
// GetOrderListRequest 获取全部订单列表请求
type GetOrderListRequest struct {
}
// GetOrderByUserIDRequest 根据用户ID查询订单列表
type GetOrderByUserIDRequest struct {
UserID uint `form:"user_id" binding:"required"`
}
internal/mq/message.go
package mq
// OrderMessage 定义发送到RabbitMQ的订单消息结构
type OrderMessage struct {
UserId uint `json:"user_id"` // 用户ID
SeckillId uint `json:"seckill_id"` // 秒杀活动ID
ProductId uint `json:"product_id"` // 商品ID
Amount float64 `json:"amount"` // 订单金额
}
四. 订单Repository
internal/repository/order.go
package repository
import (
"github.com/Chuan81/secgo-mall/internal/model"
)
// CreateOrder 创建订单
func CreateOrder(order *model.Order) error {
return DB.Create(order).Error
}
// GetOrderByID 根据ID获取订单
func GetOrderByID(id uint) (*model.Order, error) {
var order model.Order
err := DB.First(&order, id).Error
return &order, err
}
// GetOrderByUserID 根据用户ID获取订单列表
func GetOrderByUserID(userId uint) ([]*model.Order, error) {
var orders []*model.Order
err := DB.Where("user_id = ?", userId).Find(&orders).Error
return orders, err
}
// UpdateOrderStatus 更新订单状态
func UpdateOrderStatus(orderId uint, status int) error {
return DB.Model(&model.Order{}).Where("order_id = ?", orderId).Update("status", status).Error
}
五. 订单Service
internal/service/order.go
package service
import (
"github.com/Chuan81/secgo-mall/internal/model"
"github.com/Chuan81/secgo-mall/internal/repository"
)
// GetOrderList 获取用户订单列表
func GetOrderList(userId uint) ([]*model.Order, error) {
return repository.GetOrderByUserID(userId)
}
六. 生产者与消费者
1) 生产者
internal/mq/producer.go
package mq
import (
"encoding/json"
amqp "github.com/rabbitmq/amqp091-go"
)
// SendOrderMessage 发送订单消息到RabbitMQ
func SendOrderMessage(msg *OrderMessage) error {
// 将订单消息反序列化为JSON
body, err := json.Marshal(msg)
if err != nil {
return err
}
// 发布消息到指定队列
err = Channel.Publish(
"", // exchange(使用默认exchange)
OrderQueue, // routing key(队列名)
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json", // 消息类型
Body: body, // 消息内容
DeliveryMode: amqp.Persistent, // 消息持久化
},
)
return err
}
2) 消费者
internal/mq/consumer.go
package mq
import (
"encoding/json"
"errors"
"log"
"github.com/Chuan81/secgo-mall/internal/model"
"gorm.io/gorm"
)
var DB *gorm.DB
func SetDB(db *gorm.DB) {
DB = db
}
// StartConsumer 启动消费者
func StartConsumer() {
// 订阅队列
msgs, err := Channel.Consume(
OrderQueue, // queue
"", // consumer
false, // auto-ack
false, // exclusive
false, // no-local
false, // no-wait
nil, // args
)
if err != nil {
log.Fatalf("Failed to register a consumer: %v", err)
}
// 启动协程处理消息
go func() {
for msg := range msgs { // 处理消息
var orderMsg OrderMessage
// 将消息反序列化为订单消息
if err := json.Unmarshal(msg.Body, &orderMsg); err != nil {
log.Printf("Failed to unmarshal message: %v", err)
msg.Nack(false, false) // 丢弃这条消息
continue
}
//
if err := CreateOrder(&orderMsg); err != nil {
log.Printf("Failed to create order: %v", err)
msg.Nack(false, true) // 重新入队,稍后重试
continue
}
msg.Ack(false) // 确认消息处理成功
log.Printf("Order created: user=%d, seckill=%d", orderMsg.UserId, orderMsg.SeckillId)
}
}()
}
// CreateOrder 消费者调用,创建订单(带事务)
func CreateOrder(msg *OrderMessage) error {
// 开启事务,保证库存扣减和订单创建原子性
tx := DB.Begin()
// 扣减商品真实库存(乐观锁,防止超卖)
// 条件: product_id匹配 且 stock > 0
result := tx.Model(&model.Product{}).
Where("product_id = ? AND stock > 0", msg.ProductId).
Update("stock", gorm.Expr("stock - 1"))
if result.Error != nil {
tx.Rollback()
return result.Error
}
if result.RowsAffected == 0 {
tx.Rollback()
return errors.New("stock not enough")
}
// 创建订单
order := &model.Order{
UserId: msg.UserId,
ProductId: msg.ProductId,
SeckillId: msg.SeckillId,
Amount: msg.Amount,
Status: 0, // 待支付
}
if err := tx.Create(order).Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
return tx.Commit().Error
}
七. 秒杀service扣减真实库存
上期我们实现internal/service/seckill.go时
我们只完成到了扣减Redis预热库存
在此基础上我们需要引入消息队列,并传入订单消息,完成MySQL中真实库存的扣减
而后GORM便会在order表中创建订单
我们只需要在internal/service/seckill.go中SeckillPurchase函数末尾处添加以下代码即可
// ...
func SeckillPurchase(seckillId, userId uint) (int, error) {
// ...
// 扣减成功,发送订单消息到MQ
orderMsg := &mq.OrderMessage{
UserId: userId,
SeckillId: seckillId,
ProductId: seckill.ProductId,
Amount: seckill.Price,
}
if err := mq.SendOrderMessage(orderMsg); err != nil {
// MQ 发送失败日志告警(库存已扣,不应回滚)
println("MQ send failed:", err.Error())
}
return 1, nil // (原先代码) 返回订单创建成功信息
}
至此我们真正完成了下单逻辑
八. 订单查询Handler
internal/api/handlers/order.go
package handlers
import (
"github.com/Chuan81/secgo-mall/internal/api/handlers/dto"
"github.com/Chuan81/secgo-mall/internal/service"
"github.com/Chuan81/secgo-mall/pkg/response"
"github.com/gin-gonic/gin"
)
// GetOrderList 获取当前登录用户的订单列表
func GetOrderList(c *gin.Context) {
uid, _ := c.Get("uid")
orderList, err := service.GetOrderList(uid.(uint))
if err != nil {
response.Fail(c, 500, "failed to get order list")
return
}
response.Sucess(c, orderList)
}
// GetOrderByUserID 根据用户ID查询订单列表(管理员用)
func GetOrderByUserID(c *gin.Context) {
var req dto.GetOrderByUserIDRequest
if uid, err := strconv.Atoi(c.Param("user_id")); err == nil {
req.UserID = uint(uid)
} else {
response.Fail(c, 400, "invalid parameters")
return
}
orderList, err := service.GetOrderList(req.UserID)
if err != nil {
response.Fail(c, 500, "failed to get order list")
return
}
response.Sucess(c, orderList)
}
九. RabbitMQ的启动与订单查询路由
cmd/secgo-mall/main.go
// ...
func main() {
// ...
// 初始化Redis连接
cache.InitRedis()
// 初始化RabbitMQ并启动消费者
mq.InitRabbitMQ() //新
mq.StartConsumer() //新
mq.SetDB(database.DB) //新
// 使用全局配置设置Gin的运行模式
gin.SetMode(config.GlobalConfig.Server.Mode)
// ...
// 需要鉴权的私有路由
protected := r.Group("/api")
protected.Use(middleware.JWTAuth()) // 使用JWT鉴权中间件
{
// ...
// 用户获取名下订单列表
protected.GET("/order/list", handlers.GetOrderList) // 新
// 管理员根据用户ID获取订单列表
protected.GET("/order/list/:user_id", handlers.GetOrderByUserID) // 新
}
// ...
}
// ...
顺利运行,在进行下一步开发前,我们先对现有的逻辑进行一些测试,详情还请跳转10.5章

浙公网安备 33010602011771号