v4: 并发借书系统 --- 安全与并发
一、版本概述
v4 在 v3 的 HTTP API 基础上引入了借阅系统 和并发控制 。核心挑战是多人同时借同一本书时的库存超卖问题,通过 sync.Mutex + context.Context 超时控制解决。还引入了优雅关闭和定时逾期检查。
相比 v3 的核心变化:
- 借阅系统:BorrowRecord 模型 + BorrowService + BorrowRepository
- 并发安全:Mutex 加锁防止超借,Context 超时控制借阅操作
- 库存分离:Book 新增 TotalStock/Available 字段,借阅扣减 Available
- 优雅关闭:signal.Notify + http.Server.Shutdown
- 定时任务:StartOverdueChecker 每 30 秒检查逾期
项目结构:
v4/
├── main.go # 入口(优雅关闭+定时任务)
├── model/
│ ├── book.go # Book 增加 TotalStock/Available
│ └── borrow_record.go # 借阅记录(新增)
├── repository/
│ ├── book_repo.go # 增加 DecreaseAvailable/IncreaseAvailable
│ └── borrow_repo.go # 借阅仓储(新增)
├── service/
│ ├── book_service.go # 图书业务
│ └── borrow_service.go # 借阅业务+并发控制(新增核心)
├── handler/
│ └── http_handler.go # 新增借阅路由
├── middleware/
│ └── logger.go # 日志+恢复
└── data/
├── books.json
└── borrows.json
二、核心代码解读(关键片段)
borrow_record.go --- 借阅模型
go
const (
StatusBorrowed = "borrowed"
StatusReturned = "returned"
StatusOverdue = "overdue"
OverdueDays = 15
)
type BorrowRecord struct {
ID int `json:"id"`
BookID int `json:"book_id"`
BookTitle string `json:"book_title"`
UserName string `json:"user_name"`
BorrowDate time.Time `json:"borrow_date"`
ReturnDate *time.Time `json:"return_date,omitempty"`
Status string `json:"status"`
IsOverdue bool `json:"is_overdue"`
}
设计要点:
- 状态机:borrowed → returned / overdue
ReturnDate使用指针*time.Time,零值时序列化为 null 而非零时间OverdueDays = 15作为逾期阈值
borrow_service.go --- 并发借阅核心
go
type borrowService struct {
repo *repository.BorrowRepository
bookRepo *repository.BookRepository
mu sync.Mutex
}
func (s *borrowService) BorrowBook(ctx context.Context, bookID int, userName string) (*model.BorrowRecord, error) {
// 1. 加锁 --- 防止并发超借
s.mu.Lock()
defer s.mu.Unlock()
// 2. Context 超时检查
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// 3. 检查库存
book, err := s.bookRepo.GetBookByID(bookID)
if err != nil {
return nil, err
}
if book.Available <= 0 {
return nil, ErrBookNotAvailable
}
// 4. 扣减库存
if err := s.bookRepo.DecreaseAvailable(bookID); err != nil {
return nil, err
}
// 5. 创建借阅记录(失败时回滚库存)
record, err := s.repo.AddRecord(bookID, book.Title, userName)
if err != nil {
s.bookRepo.IncreaseAvailable(bookID) // 回滚
return nil, err
}
return record, nil
}
设计要点:
- Mutex 加锁:同一时刻只有一个借阅操作进入临界区,防止"读到库存为1 → 两个请求都通过"的超卖
- Context 超时 :Handler 层设置
context.WithTimeout5秒,超时自动取消 - 补偿事务 :创建记录失败时调用
IncreaseAvailable回滚库存 - 锁粒度:使用全局 Mutex(简化实现),v5 会升级为分布式锁
main.go --- 优雅关闭
go
// 监听系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 优雅关闭
<-quit
fmt.Println("\n🛑 正在关闭服务...")
cancel() // 取消定时任务
ctx, shutdown := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdown()
srv.Shutdown(ctx) // 等待现有请求完成
StartOverdueChecker --- 定时任务
go
func (s *borrowService) StartOverdueChecker(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
go func() {
for {
select {
case <-ticker.C:
s.checkAndUpdateOverdue()
case <-ctx.Done():
ticker.Stop()
return
}
}
}()
}
三、与 v3 的对比
| 特性 | v3 | v4 |
|---|---|---|
| 业务范围 | 仅图书管理 | 图书+借阅 |
| 并发控制 | 无 | Mutex + Context |
| 库存模型 | 无 | TotalStock/Available |
| 优雅关闭 | 无 | signal + Shutdown |
| 定时任务 | 无 | Ticker + select |
| 错误恢复 | middleware | middleware + 补偿事务 |
四、设计思想
| 思想 | 体现 |
|---|---|
| 并发安全 | Mutex 保护共享状态,防止竞态条件 |
| 超时控制 | Context 传播取消信号,防止操作阻塞 |
| 补偿事务 | 失败时回滚已执行的操作,保证数据一致性 |
| 优雅关闭 | 收到信号后等待请求完成,避免数据丢失 |
| 定时任务 | Ticker + select + ctx.Done() 可取消的循环 |
五、为什么需要 v5?
v4 实现了并发借阅,但存在架构级限制:
- 单进程:所有功能在一个进程中,无法独立扩缩容
- 内存存储:重启丢失数据(虽然会保存到文件,但不适合生产)
- 全局锁:Mutex 只能保护单进程内的并发,多实例部署无效
- 无缓存:每次查询都从文件读取,性能受限
- 服务间强耦合:BookService 和 BorrowService 在同一进程
v5 的改进方向:微服务拆分 + MySQL 持久化 + Redis 缓存/分布式锁 + gRPC 通信。
##六、补充内容:
bash
v4 API 测试指南
一、启动服务
bash
cd H:/libraries-pm/go-library
go run ./v4
服务启动后,默认监听 http://localhost:8080
二、API 列表
方法 路径 功能 分类
POST /books 添加图书 图书管理
GET /books 查询所有图书 图书管理
GET /books/{id} 查询单本图书 图书管理
PUT /books/{id} 更新图书 图书管理
DELETE /books/{id} 删除图书 图书管理
POST /books/save 保存到文件 图书管理
POST /books/{id}/borrow 借书 借阅管理(v4新增)
POST /books/{id}/return 还书 借阅管理(v4新增)
GET /books/{id}/status 查询图书状态 借阅管理(v4新增)
GET /borrows 借阅历史 借阅管理(v4新增)
GET /health 健康检查 系统
三、测试步骤
3.1 健康检查
bash
curl http://localhost:8080/health
期望返回:
json
{"code":0,"message":"success","data":{"status":"ok","version":"v4.0"}}
3.2 添加图书(含库存)
bash
# 添加一本库存为3的图书
curl -X POST http://localhost:8080/books \
-H "Content-Type: application/json" \
-d '{"title":"Go并发实战","author":"张三","price":69.00,"stock":3}'
期望返回:
json
{"code":0,"message":"success","data":{"id":1,"title":"Go并发实战","author":"张三","price":69,"total_stock":3,"available":3}}
3.3 查询所有图书
bash
curl http://localhost:8080/books
3.4 用户A借书
bash
curl -X POST http://localhost:8080/books/1/borrow \
-H "Content-Type: application/json" \
-d '{"user_name":"张三"}'
期望返回(库存从3变为2):
json
{"code":0,"message":"success","data":{"id":1,"book_id":1,"book_title":"Go并发实战","user_name":"张三","status":"borrowed",...}}
3.5 用户B借书(同一本书)
bash
curl -X POST http://localhost:8080/books/1/borrow \
-H "Content-Type: application/json" \
-d '{"user_name":"李四"}'
3.6 查询图书状态
bash
curl http://localhost:8080/books/1/status
期望返回(显示库存和借阅情况):
json
{
"code":0,
"message":"success",
"data":{
"id":1,
"title":"Go并发实战",
"total_stock":3,
"available":1,
"borrowed_count":2,
"borrow_records":[...]
}
}
3.7 用户A还书
bash
curl -X POST http://localhost:8080/books/1/return \
-H "Content-Type: application/json" \
-d '{"user_name":"张三"}'
期望返回:
json
{"code":0,"message":"success","data":{"message":"还书成功"}}
3.8 查询借阅历史
bash
# 查询某本书的借阅历史
curl "http://localhost:8080/borrows?book_id=1"
# 查询逾期记录(不传参数默认返回逾期)
curl http://localhost:8080/borrows
四、并发测试(v4 核心功能)
4.1 模拟100人抢1本书
bash
# 1. 先添加一本库存为1的图书
curl -X POST http://localhost:8080/books \
-H "Content-Type: application/json" \
-d '{"title":"限量版图书","author":"测试","price":99.00,"stock":1}'
# 2. 使用脚本并发借书(Linux/Mac)
for i in {1..100}; do
curl -X POST http://localhost:8080/books/2/borrow \
-H "Content-Type: application/json" \
-d "{\"user_name\":\"user$i\"}" &
done
wait
# 3. 查询状态,确认只有1人借到
curl http://localhost:8080/books/2/status
4.2 Windows 并发测试(使用 PowerShell)
powershell
# 在 PowerShell 中执行
1..100 | ForEach-Object {
$body = @{user_name="user$_"} | ConvertTo-Json
Invoke-RestMethod -Uri "http://localhost:8080/books/2/borrow" -Method POST -Body $body -ContentType "application/json"
} | Measure-Object
五、竞态检测
bash
# 使用 -race 检测数据竞争
go test -race ./v4/...
六、完整测试脚本
bash
#!/bin/bash
echo "=========================================="
echo " 📚 v4 API 测试"
echo "=========================================="
echo -e "\n1️⃣ 健康检查"
curl -s http://localhost:8080/health | jq .
echo -e "\n2️⃣ 添加图书(库存3本)"
curl -s -X POST http://localhost:8080/books \
-H "Content-Type: application/json" \
-d '{"title":"Go并发实战","author":"张三","price":69.00,"stock":3}' | jq .
echo -e "\n3️⃣ 查询所有图书"
curl -s http://localhost:8080/books | jq .
echo -e "\n4️⃣ 张三借书"
curl -s -X POST http://localhost:8080/books/1/borrow \
-H "Content-Type: application/json" \
-d '{"user_name":"张三"}' | jq .
echo -e "\n5️⃣ 李四借书"
curl -s -X POST http://localhost:8080/books/1/borrow \
-H "Content-Type: application/json" \
-d '{"user_name":"李四"}' | jq .
echo -e "\n6️⃣ 图书状态"
curl -s http://localhost:8080/books/1/status | jq .
echo -e "\n7️⃣ 张三还书"
curl -s -X POST http://localhost:8080/books/1/return \
-H "Content-Type: application/json" \
-d '{"user_name":"张三"}' | jq .
echo -e "\n8️⃣ 再次查询状态"
curl -s http://localhost:8080/books/1/status | jq .
echo -e "\n9️⃣ 借阅历史"
curl -s "http://localhost:8080/borrows?book_id=1" | jq .
echo -e "\n✅ 测试完成!"
七、测试要点总结
测试场景 预期结果 验证方法
正常借书 库存减1,生成借阅记录 查看返回数据
库存不足时借书 返回错误 库存不足 查看错误信息
100人抢1本书 仅1人成功,99人失败 查看状态 available=0
正常还书 库存加1,记录状态变更 查询状态
不存在的用户还书 返回错误 未找到借阅记录 查看错误信息
逾期检查 日志打印逾期记录 观察服务端日志