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,记录状态变更	查询状态
不存在的用户还书	返回错误 未找到借阅记录	查看错误信息
逾期检查	日志打印逾期记录	观察服务端日志
相关推荐
前端百草阁1 小时前
JavaScript 设计模式(23 种)
开发语言·前端·javascript·设计模式
用户6757049885021 小时前
Redis 分布式锁的 5个坑,真的是又大又深!!
redis·后端
用户6757049885021 小时前
Go 程序员进阶必读:这 24 个经典的 Golang 错误,连老手都会栽跟头!
后端·go
EntyIU1 小时前
Java NIO 实战
java·开发语言·nio
用户69371750013842 小时前
AI 领域的 Harness,到底是什么意思?
android·前端·后端
二宝哥2 小时前
springboot项目使用Gradle工具实现依赖版本控制
java·spring boot·后端·gradle·版本
ITender2 小时前
Nacos 迁移引发 Full GC 问题深度分析
后端·面试
Gatlin2 小时前
逆向学习:我为什么放着文档不看,直接读字节码
后端
我不是外星人2 小时前
一键还原任意网站,这套 Playwright + 多 Agent 工程流太猛了
前端·后端·ai编程
浩瀚之水_csdn2 小时前
Python 3 网络编程详解:从原理到实战
开发语言·网络·python