go语言项目--实例化(图书管理)--004

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.WithTimeout 5秒,超时自动取消
  • 补偿事务 :创建记录失败时调用 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 实现了并发借阅,但存在架构级限制:

  1. 单进程:所有功能在一个进程中,无法独立扩缩容
  2. 内存存储:重启丢失数据(虽然会保存到文件,但不适合生产)
  3. 全局锁:Mutex 只能保护单进程内的并发,多实例部署无效
  4. 无缓存:每次查询都从文件读取,性能受限
  5. 服务间强耦合: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,记录状态变更	查询状态
不存在的用户还书	返回错误 未找到借阅记录	查看错误信息
逾期检查	日志打印逾期记录	观察服务端日志
相关推荐
牛奔7 小时前
Go 如何打印调试深层或嵌套的结构体
开发语言·后端·golang
红烧大青虫7 小时前
HarmonyOS应用开发实战:小事记 - 数据迁移策略:RDB 表结构变更的版本号管理与 onUpgrade 回调
后端·华为·harmonyos·鸿蒙系统
geovindu8 小时前
go: Iterative Algorithms
开发语言·后端·算法·golang·迭代算法
学逆向的8 小时前
汇编——JCC指令
开发语言·汇编·网络安全
mayaairi9 小时前
JS循环语句深度解析:嵌套for、while与do...while
开发语言·前端·javascript
阳光是sunny9 小时前
LangGraph实战教程:defer延迟节点——让收尾工作自动排到最后
前端·人工智能·后端
前端工作日常9 小时前
我学习到的Java中domain和dto区别
java·后端
kite01219 小时前
Go语言Map深度解析与最佳实践
开发语言·后端·golang
阳光是sunny10 小时前
LangGraph实战教程:控制流详解
前端·人工智能·后端
hold?fish:palm11 小时前
kv存储主从复制的设计与实现
c++·redis·后端