go语言项目--实例化(图书管理)--004 v4: 并发借书系统 — 安全与并发一、版本概述v4 在 v3 的 HTTP API 基础上引入了借阅系统和并发控制。核心挑战是多人同时借同一本书时的库存超卖问题通过sync.Mutexcontext.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 — 借阅模型const(StatusBorrowedborrowedStatusReturnedreturnedStatusOverdueoverdueOverdueDays15)typeBorrowRecordstruct{IDintjson:idBookIDintjson:book_idBookTitlestringjson:book_titleUserNamestringjson:user_nameBorrowDate time.Timejson:borrow_dateReturnDate*time.Timejson:return_date,omitemptyStatusstringjson:statusIsOverduebooljson:is_overdue}设计要点状态机borrowed → returned / overdueReturnDate使用指针*time.Time零值时序列化为 null 而非零时间OverdueDays 15作为逾期阈值borrow_service.go — 并发借阅核心typeborrowServicestruct{repo*repository.BorrowRepository bookRepo*repository.BookRepository mu sync.Mutex}func(s*borrowService)BorrowBook(ctx context.Context,bookIDint,userNamestring)(*model.BorrowRecord,error){// 1. 加锁 — 防止并发超借s.mu.Lock()defers.mu.Unlock()// 2. Context 超时检查select{case-ctx.Done():returnnil,ctx.Err()default:}// 3. 检查库存book,err:s.bookRepo.GetBookByID(bookID)iferr!nil{returnnil,err}ifbook.Available0{returnnil,ErrBookNotAvailable}// 4. 扣减库存iferr:s.bookRepo.DecreaseAvailable(bookID);err!nil{returnnil,err}// 5. 创建借阅记录失败时回滚库存record,err:s.repo.AddRecord(bookID,book.Title,userName)iferr!nil{s.bookRepo.IncreaseAvailable(bookID)// 回滚returnnil,err}returnrecord,nil}设计要点Mutex 加锁同一时刻只有一个借阅操作进入临界区防止读到库存为1 → 两个请求都通过的超卖Context 超时Handler 层设置context.WithTimeout5秒超时自动取消补偿事务创建记录失败时调用IncreaseAvailable回滚库存锁粒度使用全局 Mutex简化实现v5 会升级为分布式锁main.go — 优雅关闭// 监听系统信号quit:make(chanos.Signal,1)signal.Notify(quit,syscall.SIGINT,syscall.SIGTERM)// 优雅关闭-quit fmt.Println(\n 正在关闭服务...)cancel()// 取消定时任务ctx,shutdown:context.WithTimeout(context.Background(),5*time.Second)defershutdown()srv.Shutdown(ctx)// 等待现有请求完成StartOverdueChecker — 定时任务func(s*borrowService)StartOverdueChecker(ctx context.Context){ticker:time.NewTicker(30*time.Second)gofunc(){for{select{case-ticker.C:s.checkAndUpdateOverdue()case-ctx.Done():ticker.Stop()return}}}()}三、与 v3 的对比特性v3v4业务范围仅图书管理图书借阅并发控制无Mutex Context库存模型无TotalStock/Available优雅关闭无signal Shutdown定时任务无Ticker select错误恢复middlewaremiddleware 补偿事务四、设计思想思想体现并发安全Mutex 保护共享状态防止竞态条件超时控制Context 传播取消信号防止操作阻塞补偿事务失败时回滚已执行的操作保证数据一致性优雅关闭收到信号后等待请求完成避免数据丢失定时任务Ticker select ctx.Done() 可取消的循环五、为什么需要 v5v4 实现了并发借阅但存在架构级限制单进程所有功能在一个进程中无法独立扩缩容内存存储重启丢失数据虽然会保存到文件但不适合生产全局锁Mutex 只能保护单进程内的并发多实例部署无效无缓存每次查询都从文件读取性能受限服务间强耦合BookService 和 BorrowService 在同一进程v5 的改进方向微服务拆分 MySQL 持久化 Redis 缓存/分布式锁 gRPC 通信。##六、补充内容v4 API 测试指南 一、启动服务bashcdH:/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健康检查bashcurlhttp://localhost:8080/health 期望返回 json{code:0,message:success,data:{status:ok,version:v4.0}}3.2添加图书含库存bash# 添加一本库存为3的图书curl-XPOST http://localhost:8080/books\-HContent-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查询所有图书bashcurlhttp://localhost:8080/books3.4用户A借书bashcurl-XPOST http://localhost:8080/books/1/borrow\-HContent-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借书同一本书bashcurl-XPOST http://localhost:8080/books/1/borrow\-HContent-Type: application/json\-d{user_name:李四}3.6查询图书状态bashcurlhttp://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还书bashcurl-XPOST http://localhost:8080/books/1/return\-HContent-Type: application/json\-d{user_name:张三}期望返回 json{code:0,message:success,data:{message:还书成功}}3.8查询借阅历史bash# 查询某本书的借阅历史curlhttp://localhost:8080/borrows?book_id1# 查询逾期记录不传参数默认返回逾期curlhttp://localhost:8080/borrows 四、并发测试v4 核心功能4.1模拟100人抢1本书bash# 1. 先添加一本库存为1的图书curl-XPOST http://localhost:8080/books\-HContent-Type: application/json\-d{title:限量版图书,author:测试,price:99.00,stock:1}# 2. 使用脚本并发借书Linux/Macforiin{1..100};docurl-XPOST http://localhost:8080/books/2/borrow\-HContent-Type: application/json\-d{\user_name\:\user$i\}donewait# 3. 查询状态确认只有1人借到curlhttp://localhost:8080/books/2/status4.2Windows 并发测试使用 PowerShell powershell# 在 PowerShell 中执行1..100|ForEach-Object{$body{user_nameuser$_}|ConvertTo-Json Invoke-RestMethod-Urihttp://localhost:8080/books/2/borrow-MethodPOST-Body$body-ContentTypeapplication/json}|Measure-Object 五、竞态检测bash# 使用 -race 检测数据竞争gotest-race./v4/... 六、完整测试脚本bash#!/bin/bashechoecho v4 API 测试echoecho-e\n1️⃣ 健康检查curl-shttp://localhost:8080/health|jq.echo-e\n2️⃣ 添加图书库存3本curl-s-XPOST http://localhost:8080/books\-HContent-Type: application/json\-d{title:Go并发实战,author:张三,price:69.00,stock:3}|jq.echo-e\n3️⃣ 查询所有图书curl-shttp://localhost:8080/books|jq.echo-e\n4️⃣ 张三借书curl-s-XPOST http://localhost:8080/books/1/borrow\-HContent-Type: application/json\-d{user_name:张三}|jq.echo-e\n5️⃣ 李四借书curl-s-XPOST http://localhost:8080/books/1/borrow\-HContent-Type: application/json\-d{user_name:李四}|jq.echo-e\n6️⃣ 图书状态curl-shttp://localhost:8080/books/1/status|jq.echo-e\n7️⃣ 张三还书curl-s-XPOST http://localhost:8080/books/1/return\-HContent-Type: application/json\-d{user_name:张三}|jq.echo-e\n8️⃣ 再次查询状态curl-shttp://localhost:8080/books/1/status|jq.echo-e\n9️⃣ 借阅历史curl-shttp://localhost:8080/borrows?book_id1|jq.echo-e\n✅ 测试完成七、测试要点总结 测试场景 预期结果 验证方法 正常借书 库存减1生成借阅记录 查看返回数据 库存不足时借书 返回错误 库存不足 查看错误信息100人抢1本书 仅1人成功99人失败 查看状态available0正常还书 库存加1记录状态变更 查询状态 不存在的用户还书 返回错误 未找到借阅记录 查看错误信息 逾期检查 日志打印逾期记录 观察服务端日志