【无标题】

go-redis/gin/gorm 分布式锁的简单小测试,一目了然

应用
IP请求接口描述
192.168.40.180/181GET/add?gid=商品ID&client_id=请求任务ID库存加一
192.168.40.180/181GET/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(每次都不一样),数据就会异常。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值