go语言的gorm事务中使用redsync锁锁不住
在gorm的事务中我开启了20个协程模仿用户删除 但是redsync.Lock()的互斥性消失了 导致都获取了锁
如果我把事务关闭 互斥性就恢复正常了
package main
import (
"fmt"
goredislib "github.com/go-redis/redis/v8"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v8"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"os"
"sync"
"time"
)
var DB *gorm.DB
type BaseModel struct {
ID int32 `gorm:"primary_key;comment:ID" json:"id"`
CreatedAt time.Time `gorm:"column:add_time;comment:创建时间" json:"-"`
UpdatedAt time.Time `gorm:"column:update_time;comment:更新时间" json:"-"`
DeletedAt gorm.DeletedAt `gorm:"comment:删除时间" json:"-"`
IsDeleted bool `gorm:"comment:是否删除" json:"-"`
}
type Inventory struct {
BaseModel
Goods int32 `gorm:"type:int;index;comment:商品id"`
Stocks int32 `gorm:"type:int;comment:仓库"`
Version int32 `gorm:"type:int;comment:分布式锁-乐观锁"`
}
func InitDB() {
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
"root", "123456", "localhost", 3306, "mxshop_inventory_srv2")
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: logger.Info, // 日志级别
//LogLevel: logger.Silent, // 日志级别
//IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound(记录未找到)错误
Colorful: true, // 禁用彩色打印
},
)
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
var err error
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
SingularTable: true,
},
Logger: newLogger,
})
if err != nil {
panic(err)
}
}
func main() {
InitDB()
client := goredislib.NewClient(&goredislib.Options{
Addr: "1.1.1.1:6301",
})
pool := goredis.NewPool(client)
rs := redsync.New(pool)
gNum := 20
var wg sync.WaitGroup
wg.Add(gNum)
DB.Transaction(func(tx *gorm.DB) error {
for i := 0; i < gNum; i++ {
go func() {
defer wg.Done()
var inv Inventory
mutex := rs.NewMutex(fmt.Sprintf("goodsss_%d", 421))
if err := mutex.Lock(); err != nil {
fmt.Println("获取redis分布式锁异常-1")
}
if result := DB.Where(&Inventory{Goods: int32(421)}).First(&inv); result.RowsAffected == 0 {
panic("库存信息不存在")
}
fmt.Println(inv.Stocks)
if err := tx.Model(&Inventory{}).Select("Stocks").Where("goods = ?", int32(421)).Update("stocks", inv.Stocks-1); err.RowsAffected == 0 {
fmt.Println("更新失败:", err.Error.Error())
fmt.Println(inv.Stocks)
}
if ok, err := mutex.Unlock(); !ok || err != nil {
fmt.Println("释放redis分布式锁异常-4")
}
}()
}
return nil
})
wg.Wait()
}
这个问题可能是由于在gorm事务中使用redsync锁导致的。因为gorm事务本身已经提供了互斥性保证,因此再使用redsync锁就可能会出现冲突。
解决方案是在事务外部使用redsync锁,并将事务操作放在redsync锁的内部。这样可以确保在同一时刻只有一个操作可以访问数据库,从而避免并发冲突。
补充:如果遇到redis的事务内,执行的修改只有第一条生效的情况,可以尝试以下几种方法来解决:
使用redis的watch命令监控关键字,并在关键字发生改变时重试事务。以下是使用 Redis 的 watch 命令实现事务的示例代码:
import redis
# 连接 Redis
r = redis.Redis(host='localhost', port=6379, db=0)
# 设置关键字的初始值
r.set('key', 100)
while True:
# 开启事务
with r.pipeline() as pipe:
# 监控关键字
try:
pipe.watch('key')
current_value = int(r.get('key'))
# 对关键字进行操作
updated_value = current_value - 10
pipe.multi()
pipe.set('key', updated_value)
# 提交事务
pipe.execute()
break
except redis.WatchError:
# 关键字发生改变,重试事务
continue
该代码在事务内使用 watch 命令监控关键字,在关键字发生改变时会重试事务。
使用redis的乐观锁机制,在修改前先检查关键字的值是否已经发生改变。
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
def update_value(key, value):
while True:
# 获取关键字的当前值
current_value = r.get(key)
# 对关键字执行CAS操作,判断关键字的值是否已经发生改变
if r.getset(key, value) == current_value:
# 如果关键字的值未发生改变,说明事务执行成功
return True
# 否则说明关键字的值已经发生改变,需要重试事务
# 调用update_value函数,更新关键字的值
update_value("key", "new_value")
将多条记录分成多个事务进行处理,避免事务内多条记录产生冲突。
为避免事务内多条记录的冲突,可以将多条记录分成多个事务进行处理。
for record in records:
redis_conn.multi()
redis_conn.decrby(record["key"], record["value"])
redis_conn.exec()
在上面的代码中,对于每条记录,都创建一个独立的事务,在事务内进行扣减操作,这样就可以避免冲突了。
补充2:可以举一个订单系统的例子,假设有多个用户在同时购买同一件商品,如果没有使用事务,那么同时进行扣减库存操作时会出现多次扣减,导致库存减少过多。使用 Redis 事务可以保证在一次事务中,扣减库存操作是原子性的,避免了库存减少过多的情况。但是如果使用的 Redis 事务不是严格的互斥锁,那么多个事务可能同时执行,导致同样的问题。这就需要使用 Redis 分布式锁来避免。如果你想使用事务来保证 Redis 在批量扣减操作中的原子性,可以使用 Redis 事务机制,下面是一个使用 redis-py 库实现的示例代码:
import redis
redis_conn = redis.Redis(host='localhost', port=6379, db=0)
# 开启事务
pipeline = redis_conn.pipeline()
pipeline.multi()
# 执行扣减操作
pipeline.decrby('key1', 10)
pipeline.decrby('key2', 20)
# 执行事务
pipeline.execute()
如果事务在执行过程中遇到任何异常,可以通过捕获异常来回滚事务。
import redis
redis_conn = redis.Redis(host='localhost', port=6379, db=0)
try:
# 开启事务
pipeline = redis_conn.pipeline()
pipeline.multi()
# 执行扣减操作
pipeline.decrby('key1', 10)
pipeline.decrby('key2', 20)
# 执行事务
pipeline.execute()
except Exception as e:
# 回滚事务
pipeline.discard()
print("Transaction failed:", e)
在事务内使用redsync锁失效可能是因为事务中数据被锁定,导致锁不能正常生效,你可以尝试在事务外使用redsync锁来解决问题。如果你仍然需要在事务内使用redsync锁,你可以尝试使用类似"SELECT ... FOR UPDATE"语句,以保证在事务内部数据被锁定