v5: 图书微服务 --- 分布式架构
一、版本概述
v5 将 v4 的单体应用拆分为两个独立微服务(book-service 和 borrow-service),通过 gRPC 通信。引入 MySQL 替代内存存储,Redis 实现缓存和分布式锁。
相比 v4 的核心变化:
- 微服务拆分:book-service(:8081/:9081) + borrow-service(:8082)
- MySQL 持久化:GORM ORM,AutoMigrate 自动建表
- Redis 缓存:Cache-Aside 模式,5分钟过期
- 分布式锁:Redis SetNX 实现跨实例互斥
- gRPC 通信:borrow-service 通过 gRPC 调用 book-service
- Protobuf 定义:独立 proto/book.proto 定义服务契约
- Docker 支持:Dockerfile + docker-compose.yml
项目结构:
v5/
├── book-service/
│ ├── main.go # HTTP(:8081) + gRPC(:9081)
│ ├── model/book.go # GORM Book 模型
│ ├── repository/
│ │ ├── db.go # MySQL 初始化
│ │ └── book_repo.go # GORM CRUD + 行锁
│ ├── cache/redis.go # Redis 缓存 + 分布式锁
│ ├── service/book_service.go # 缓存+锁+业务
│ ├── handler/
│ │ ├── http_handler.go # HTTP API
│ │ └── grpc_handler.go # gRPC 服务端
│ └── Dockerfile
├── borrow-service/
│ ├── main.go # HTTP(:8082) + gRPC Client
│ ├── model/borrow_record.go
│ ├── repository/
│ │ ├── db.go
│ │ └── borrow_repo.go
│ ├── client/grpc_client.go # gRPC 客户端封装
│ ├── service/borrow_service.go
│ ├── handler/http_handler.go
│ └── Dockerfile
├── proto/
│ ├── book.proto # Protobuf 服务定义
│ └── book/ # 生成的 Go 代码
└── docker-compose.yml
二、核心代码解读
proto/book.proto --- 服务契约
protobuf
service BookService {
rpc GetBook(GetBookRequest) returns (BookResponse);
rpc DecreaseStock(DecreaseStockRequest) returns (StockResponse);
rpc IncreaseStock(IncreaseStockRequest) returns (StockResponse);
}
设计要点:
- 只暴露 borrow-service 需要调用的方法,不暴露 CRUD
DecreaseStock/IncreaseStock原子操作库存,而非先 GetBook 再修改
cache/redis.go --- Cache-Aside + 分布式锁
go
// Cache-Aside 读缓存
func GetBookCache(ctx context.Context, id int64) (*model.Book, error) {
key := fmt.Sprintf("book:%d", id)
data, err := rdb.Get(ctx, key).Bytes()
if err != nil {
return nil, err // 缓存未命中
}
var book model.Book
json.Unmarshal(data, &book)
return &book, nil
}
// Cache-Aside 写缓存
func SetBookCache(ctx context.Context, book *model.Book) {
key := fmt.Sprintf("book:%d", book.ID)
data, _ := json.Marshal(book)
rdb.Set(ctx, key, data, 5*time.Minute)
}
// 分布式锁
func AcquireLock(ctx context.Context, key string) (bool, error) {
lockKey := fmt.Sprintf("lock:%s", key)
return rdb.SetNX(ctx, lockKey, "1", 5*time.Second).Result()
}
设计要点:
- Cache-Aside:读时先查缓存 → miss 查 DB → 写回缓存;写时更新 DB → 删除缓存
- 分布式锁:SetNX(不存在才设置)+ 过期时间(防止死锁),比 v4 的 Mutex 支持多实例
repository/book_repo.go --- GORM + 行锁
go
func (r *BookRepository) DecreaseStock(bookID int64) (int, error) {
tx := r.db.Begin()
var book model.Book
// SELECT ... FOR UPDATE 行锁
if err := tx.Set("gorm:query_option", "FOR UPDATE").First(&book, bookID).Error; err != nil {
tx.Rollback()
return 0, ErrBookNotFound
}
if book.Stock <= 0 {
tx.Rollback()
return 0, ErrStockNotEnough
}
book.Stock -= 1
tx.Save(&book)
tx.Commit()
return book.Stock, nil
}
设计要点:
- 行锁 FOR UPDATE:在事务内锁定该行,其他事务等待,防止数据库层面超卖
- 事务:Begin → 操作 → Commit/Rollback,保证原子性
- 双重保护:分布式锁(应用层)+ 行锁(数据库层)
borrow-service/client/grpc_client.go --- gRPC 调用
go
type BookServiceClient struct {
conn *grpc.ClientConn
client pb.BookServiceClient
}
func (c *BookServiceClient) DecreaseStock(ctx context.Context, bookID int64) (int, error) {
resp, err := c.client.DecreaseStock(ctx, &pb.DecreaseStockRequest{BookId: bookID})
if err != nil {
return 0, err
}
return int(resp.Available), nil
}
borrow_service.go --- 跨服务事务
go
func (s *borrowService) BorrowBook(ctx context.Context, ...) (*model.BorrowRecord, error) {
// 1. 通过 gRPC 调用 book-service 减库存
if _, err := s.grpcClient.DecreaseStock(ctx, bookID); err != nil {
return nil, ErrBookNotAvailable
}
// 2. 本地创建借阅记录
record, err := s.repo.Create(bookID, bookTitle, userName)
if err != nil {
// 3. 创建失败 → 回滚库存(补偿事务)
s.grpcClient.IncreaseStock(ctx, bookID)
return nil, err
}
return record, nil
}
设计要点:
- Saga 模式:跨服务没有强一致性事务,使用补偿事务保证最终一致性
- gRPC 同步调用:borrow-service 先调 book-service 扣库存,成功后再创建本地记录
三、与 v4 的对比
| 特性 | v4 | v5 |
|---|---|---|
| 架构 | 单体 | 微服务拆分 |
| 存储 | 内存+JSON文件 | MySQL + Redis |
| 并发控制 | 进程内 Mutex | 分布式锁 + DB行锁 |
| 服务通信 | 函数调用 | gRPC |
| 缓存 | 无 | Redis Cache-Aside |
| 部署 | 单进程 | Docker Compose |
| 跨服务事务 | 不需要 | 补偿事务(Saga) |
四、设计思想
| 思想 | 体现 |
|---|---|
| 微服务拆分 | 按业务领域拆分,独立部署和扩缩容 |
| 服务契约 | Protobuf 定义接口,强类型、向后兼容 |
| Cache-Aside | 缓存与DB分离,先更新DB再删缓存 |
| 分布式锁 | Redis SetNX 实现跨实例互斥 |
| 双重保护 | 应用层锁 + 数据库行锁,防御性编程 |
| 补偿事务 | 跨服务无强一致性,通过回滚保证最终一致 |
五、为什么需要 v6?
v5 实现了微服务架构,但仍存在不足:
- 没有用户体系:借阅没有登录认证,任何人可以借任何书
- 没有搜索功能:只能遍历查询,不支持关键词搜索
- 没有前端界面:只能通过 curl/API 工具操作
- gRPC 通信不完整:borrow-service 的 gRPC 客户端存在 placeholder
- 缺少 CORS:前端无法跨域访问 API
v6 的改进方向:新增 user-service + JWT 认证 + Bleve 全文搜索 + 前端页面 + 完善 gRPC 调用。