go-redis/gin/gorm 分布式锁的简单小测试,一目了然
应用
IP | 请求 | 接口 | 描述 |
---|---|---|---|
192.168.40.180/181 | GET | /add?gid=商品ID&client_id=请求任务ID | 库存加一 |
192.168.40.180/181 | GET | /sub?gid=商品ID&client_id=请求任务ID | 库存减一 |
nginx
192.168.40.182
配置文件参考:
[root@apollo-182 work]# cat /app/nginx/conf/nginx.conf
# nginx.conf
worker_processes auto;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
# 定义 upstream 负载均衡组
upstream backend {
server 192.168.40.180:8001; # 后端应用 1
server 192.168.40.181:8001; # 后端应用 2
}
server {
listen 80; # 监听80端口
server_name mypro.com; # 替换为你的域名或IP地址
# 负载均衡到后端服务
location / {
proxy_pass http://backend; # 将请求转发到 upstream 组
proxy_set_header Host $host; # 保持主机头
proxy_set_header X-Real-IP $remote_addr; # 转发真实IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 转发的真实IP
proxy_set_header X-Forwarded-Proto $scheme; # 转发协议
}
}
}
mysql数据库
192.168.40.199:13306
redis集群
"192.168.40.180:7000",
"192.168.40.180:7001",
"192.168.40.180:7002",
"192.168.40.181:7003",
"192.168.40.181:7004",
"192.168.40.181:7005",
代码结构
D:.
│ go.mod
│ go.sum
│ main.go
package main
import (
"com.test.www/controller"
"com.test.www/db"
"com.test.www/globol"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"log"
"os"
)
var dbClient *gorm.DB
func init() {
dbClient = db.InitDbClient()
err := dbClient.AutoMigrate(&globol.Product{})
if err != nil {
log.Printf("create db failed")
return
}
// 首次运行,随便选一个服务创建实验的数据
//dbClient.Create(&globol.Product{
// Gid: "1",
// Name: "apple",
// Price: 100,
// Inventory: 10,
//})
//dbClient.Create(&globol.Product{
// Gid: "2",
// Name: "orange",
// Price: 88,
// Inventory: 5,
//})
}
func main() {
// Open a log file to record logs
logFile, err := os.OpenFile("inventory.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Could not open log file: %v", err)
}
defer func(logFile *os.File) {
err := logFile.Close()
if err != nil {
panic(err)
}
}(logFile)
log.SetOutput(logFile)
r := gin.Default()
r.GET("/add", func(c *gin.Context) {
controller.InInventory(dbClient, c)
})
r.GET("/sub", func(c *gin.Context) {
controller.DeInventory(dbClient, c)
})
// 哪个服务器就写哪个服务器的IP地址或者使用代码获取、搞成服务发现etcd也可
err = r.Run("192.168.40.181:8001")
//err = r.Run("192.168.40.180:8001")
log.Println("Server starting on :8080...")
if err != nil {
fmt.Println(err)
return
}
}
│
├─.idea
│ .gitignore
│ com.test.www.iml
│ modules.xml
│ workspace.xml
│
├─controller
│ doInventory.go
package controller
import (
"com.test.www/db"
"com.test.www/globol"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"net/http"
)
var (
pro globol.Product
data = 1
)
// 库存加一
func InInventory(dbClient *gorm.DB, c *gin.Context) {
clientId := c.Query("client_id")
gid := c.Query("gid")
// 1,使用分布式锁模拟并发
db.UpdateInventory(clientId)
// 2,不使用分布式锁模拟高并发环境
// 打印调试信息
//time.Sleep(100 * time.Millisecond)
//log.Printf("Client %s is working\n", clientId)
res := dbClient.Model(&pro).Where("gid = ?", gid).UpdateColumn("inventory", gorm.Expr("inventory + ?", data))
// 检查是否有错误发生
if res.Error != nil {
// 处理错误
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "Failed to update inventory: " + res.Error.Error(),
})
return
}
// 检查是否有行受影响
if res.RowsAffected == 0 {
// 没有行被更新,可能是 gid 不存在
c.JSON(http.StatusNotFound, gin.H{
"code": http.StatusNotFound,
"msg": "No inventory updated, gid may not exist.",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "Add Inventory success!",
})
}
// 库存减一
func DeInventory(dbClient *gorm.DB, c *gin.Context) {
clientId := c.Query("client_id")
gid := c.Query("gid")
// 1,使用分布式锁模拟并发
db.UpdateInventory(clientId)
// 2,不使用分布式锁模拟高并发环境
// 打印调试信息
//time.Sleep(100 * time.Millisecond)
//log.Printf("Client %s is working\n", clientId)
res := dbClient.Model(&pro).Where("gid = ?", gid).UpdateColumn("inventory", gorm.Expr("inventory - ?", data))
// 检查是否有错误发生
if res.Error != nil {
// 处理错误
c.JSON(http.StatusInternalServerError, gin.H{
"code": http.StatusInternalServerError,
"msg": "Failed to update inventory: " + res.Error.Error(),
})
return
}
// 检查是否有行受影响
if res.RowsAffected == 0 {
// 没有行被更新,可能是 gid 不存在
c.JSON(http.StatusNotFound, gin.H{
"code": http.StatusNotFound,
"msg": "No inventory updated, gid may not exist.",
})
return
}
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "Reduce Inventory success!",
})
}
│
├─db
│ dbHandler.go
package db
import (
"com.test.www/globol"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func InitDbClient() *gorm.DB {
globol.Db, globol.Err = gorm.Open(mysql.Open(globol.Dsn), &gorm.Config{})
return globol.Db
}
│ redisHandler.go
package db
import (
"com.test.www/globol"
"fmt"
"github.com/redis/go-redis/v9"
"log"
"time"
)
var (
redisClusterCfg = []string{
"192.168.40.180:7000",
"192.168.40.180:7001",
"192.168.40.180:7002",
"192.168.40.181:7003",
"192.168.40.181:7004",
"192.168.40.181:7005",
}
rdb *redis.ClusterClient
)
func init() {
rdb = redis.NewClusterClient(&redis.ClusterOptions{
Addrs: redisClusterCfg,
})
rdb.Ping(globol.Ctx)
}
func AcquireLock() bool {
success, err := rdb.SetNX(globol.Ctx, globol.LockKey, globol.LockValue, globol.LockTTL).Result()
if err != nil {
fmt.Println("获取分布式锁失败:", err)
return false
}
return success
}
func ReleaseLock() {
_, err := rdb.Del(globol.Ctx, globol.LockKey).Result()
if err != nil {
fmt.Println("释放分布式锁失败:", err)
}
}
func UpdateInventory(client string) {
for {
if AcquireLock() {
// 打印
log.Printf("Client %s 取得锁,更新库存\n", client)
// 模拟操作延迟时间
time.Sleep(100 * time.Millisecond)
// 释放锁
ReleaseLock()
break
} else {
log.Printf("Client %s 没有取到锁,重试中\n\n", client)
}
// 重试时间
time.Sleep(1000 * time.Millisecond)
}
}
│
└─globol
globol.go
package globol
import (
"context"
"gorm.io/gorm"
"time"
)
var (
Ctx = context.Background()
Dsn = "dbuser1:NSD2021@tedu.cn@tcp(192.168.40.199:13306)/goods?charset=utf8mb4&parseTime=True&loc=Local"
Db *gorm.DB
Err error
)
const (
LockKey = "inventory_lock"
LockValue = "locked"
LockTTL = 3 * time.Second
)
type Product struct {
gorm.Model
Gid string `json:"gid" gorm:"gid"`
Name string `json:"name,omitempty" gorm:"name"`
Price int `json:"price,omitempty" gorm:"price"`
Inventory int `json:"inventory,omitempty" gorm:"inventory"`
}
编译传到虚拟机上
D:\com.test.www>go build -o pro_180 .
D:\com.test.www>go build -o pro_181 .
虚拟机分别启动服务
nohup ./pro_180 >> app.log 2>&1 &
nohup ./pro_181 >> app.log 2>&1 &
使用分布式锁观察过程
创建多个请求去修改数据库字段
# 使用ab简单测试
# 10个线程发起共计100次请求
ab -n 100 -c 10 http://192.168.40.182/add?gid=1&client_id=client
tail -f inventory.log
观察两台服务器的日志
- 192.168.40.180
2024/09/30 16:55:52 Client 没有取到锁,重试中
2024/09/30 16:55:52 Client 没有取到锁,重试中
2024/09/30 16:55:52 Client 没有取到锁,重试中
2024/09/30 16:55:52 Client 取得锁,更新库存
2024/09/30 16:55:53 Client 取得锁,更新库存
2024/09/30 16:55:53 Client 没有取到锁,重试中
2024/09/30 16:55:53 Client 没有取到锁,重试中
2024/09/30 16:55:54 Client 没有取到锁,重试中
2024/09/30 16:55:54 Client 取得锁,更新库存
2024/09/30 16:55:55 Client 取得锁,更新库存
。。。。。
- 192.168.40.181
2024/09/30 16:54:53 Client 没有取到锁,重试中
2024/09/30 16:54:53 Client 没有取到锁,重试中
2024/09/30 16:54:53 Client 取得锁,更新库存
2024/09/30 16:54:53 Client 取得锁,更新库存
2024/09/30 16:54:53 Client 取得锁,更新库存
2024/09/30 16:54:53 Client 取得锁,更新库存
2024/09/30 16:54:54 Client 没有取到锁,重试中
2024/09/30 16:54:54 Client 没有取到锁,重试中
2024/09/30 16:54:54 Client 没有取到锁,重试中
2024/09/30 16:54:54 Client 取得锁,更新库存
2024/09/30 16:54:55 Client 取得锁,更新库存
2024/09/30 16:54:55 Client 没有取到锁,重试中
。。。。。。。
连接redis查看锁信息
[root@apollo-181 work]# redis-cli -h 192.168.40.181 -p 7003
192.168.40.181:7003> get inventory_lock
"locked"
此时库存加到100
不使用分布式锁观察过程
# 库存重新清到0
ab -n 100 -c 10 http://192.168.40.182/add?gid=1&client_id=client_1
把涉及锁的逻辑删除,直接请求数据库。
不使用锁的结果是81(每次都不一样),数据就会异常。