新增actsaveV3方法

This commit is contained in:
liaoyulong
2026-03-06 16:39:41 +08:00
parent 713a63356e
commit 049df72520
4 changed files with 517 additions and 61 deletions

View File

@@ -2,11 +2,12 @@ package boot
import (
"context"
"time"
v1 "github.com/ayflying/utility_go/api/system/v1"
"github.com/ayflying/utility_go/service"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtimer"
)
var (
@@ -18,18 +19,31 @@ func Boot() (err error) {
// 启动计划任务定时器预防debug工具激活计划任务造成重复执行此处不执行计划任务
//err = service.SystemCron().StartCron()
//用户活动持久化每小时执行一次
service.SystemCron().AddCronV2(v1.CronType_HOUR, func(context.Context) error {
go func() {
// //用户活动持久化每小时执行一次
// service.SystemCron().AddCronV2(v1.CronType_HOUR, func(context.Context) error {
// go func() {
// err = service.GameKv().SavesV1()
// err = service.GameAct().SavesV2()
// if err != nil {
// g.Log().Error(gctx.New(), err)
// }
// }()
// return nil
// }, true)
//不放在定时器中 独立执行
//延迟10秒执行定时器
gtimer.SetTimeout(ctx, time.Second*10, func(ctx context.Context) {
gtimer.SetInterval(ctx, time.Hour*2, func(ctx context.Context) {
err = service.GameKv().SavesV1()
err = service.GameAct().SavesV2()
err = service.GameAct().SavesV3()
if err != nil {
g.Log().Error(gctx.New(), err)
}
}()
return nil
}, true)
})
})
//初始化自启动方法
for _, v := range _func {
v()

View File

@@ -306,7 +306,8 @@ func (s *sGameAct) SavesV2() (err error) {
go func() {
defer wg.Done() // Cache2SqlChan协程完成后减1
s.Cache2SqlChan(ctx, addChan, updateChan)
s.Cache2SqlChanOptimized(ctx, addChan, updateChan)
// s.Cache2SqlChan(ctx, addChan, updateChan)
}()
go func() {
@@ -314,7 +315,12 @@ func (s *sGameAct) SavesV2() (err error) {
if gtime.Now().After(RunTimeMax) {
return errors.New("redis扫描超时")
}
if keyErr := s.SaveV2Batch(ctx, keys); keyErr != nil {
// for _, v := range keys {
// if keyErr := s.SaveV2(ctx, v, addChan, updateChan); keyErr != nil {
// g.Log().Errorf(ctx, "处理key %v 失败: %v", v, keyErr)
// }
// }
if keyErr := s.SaveV2Batch(ctx, keys, addChan, updateChan); keyErr != nil {
g.Log().Errorf(ctx, "批量处理keys失败: %v", keyErr)
}
return nil
@@ -338,6 +344,58 @@ func (s *sGameAct) SavesV2() (err error) {
return
}
// SavesV3 保存游戏活动数据
//
// @Description: 保存游戏活动数据里面的超时时间时50分钟
// @receiver s *sGameAct: 游戏活动服务结构体指针
// @return err error: 返回错误信息
// SavesV3 保存游戏活动数据
func (s *sGameAct) SavesV3() (err error) {
var ctx = gctx.New()
g.Log().Debug(ctx, "开始执行游戏act数据保存了")
RunTimeMax = gtime.Now().Add(time.Minute * 50)
dataChan := make(chan *entity.GameAct, 1000)
errChan := make(chan error, 1)
var wg sync.WaitGroup
wg.Add(1) // 仅需添加1次对应Cache2SqlChan协程
// wg.Add(1) // 移除多余的Add调用避免计数不平衡
go func() {
defer wg.Done() // Cache2SqlChan协程完成后减1
s.Cache2SqlChanAll(ctx, dataChan)
// s.Cache2SqlChan(ctx, addChan, updateChan)
}()
go func() {
scanErr := tools.Redis.RedisScanV2("act:*", func(keys []string) error {
if gtime.Now().After(RunTimeMax) {
return errors.New("redis扫描超时")
}
if keyErr := s.SaveV3Batch(ctx, keys, dataChan); keyErr != nil {
g.Log().Errorf(ctx, "批量处理keys失败: %v", keyErr)
}
return nil
})
close(dataChan)
errChan <- scanErr
}()
// 等待扫描和处理完成,同时监听上下文取消
select {
case scanErr := <-errChan:
wg.Wait() // 等待Cache2SqlChan处理完剩余数据
if scanErr != nil {
return gerror.New(fmt.Sprintf("Redis扫描失败: %v", scanErr))
}
case <-ctx.Done():
wg.Wait()
return ctx.Err() // 返回上下文取消原因
}
return
}
// SaveV2 保存游戏活动数据
//
// @Description: 保存游戏活动数据
@@ -617,7 +675,7 @@ func (s *sGameAct) Cache2SqlChan(ctx context.Context, addChan, updateChan chan *
// @param ctx context.Context: 上下文对象
// @param cacheKeys []string: 缓存键列表
// @return err error: 返回错误信息
func (s *sGameAct) SaveV2Batch(ctx context.Context, cacheKeys []string) (err error) {
func (s *sGameAct) SaveV2Batch(ctx context.Context, cacheKeys []string, addChan, updateChan chan *entity.GameAct) (err error) {
if len(cacheKeys) == 0 {
return nil
}
@@ -631,6 +689,7 @@ func (s *sGameAct) SaveV2Batch(ctx context.Context, cacheKeys []string) (err err
var keyInfos []keyInfo
activeUids := make(map[int64]bool)
//提取出不活跃的信息
for _, cacheKey := range cacheKeys {
result := strings.Split(cacheKey, ":")
if len(result) < 3 {
@@ -680,7 +739,9 @@ func (s *sGameAct) SaveV2Batch(ctx context.Context, cacheKeys []string) (err err
var uids []int64
for _, ki := range validKeyInfos {
uids = append(uids, ki.uid)
if !tools.InArray[int64](uids, ki.uid) {
uids = append(uids, ki.uid)
}
}
var existingData []do.GameAct
@@ -690,14 +751,11 @@ func (s *sGameAct) SaveV2Batch(ctx context.Context, cacheKeys []string) (err err
return err
}
existMap := make(map[int64]do.GameAct)
existMap := make(map[string]do.GameAct)
for _, data := range existingData {
existMap[gconv.Int64(data.Uid)] = data
existMap[fmt.Sprintf("act:%v:%v", data.ActId, data.Uid)] = data
}
var addItems []*entity.GameAct
var updateItems []*entity.GameAct
for _, ki := range validKeyInfos {
val, ok := redisValues[ki.cacheKey]
if !ok {
@@ -707,63 +765,164 @@ func (s *sGameAct) SaveV2Batch(ctx context.Context, cacheKeys []string) (err err
if actionData == "" {
continue
}
if _, ok := existMap[ki.uid]; ok {
updateItems = append(updateItems, &entity.GameAct{
if _, ok := existMap[fmt.Sprintf("act:%d:%d", ki.actId, ki.uid)]; ok {
updateChan <- &entity.GameAct{
ActId: ki.actId,
Uid: ki.uid,
Action: actionData,
})
}
} else {
addItems = append(addItems, &entity.GameAct{
addChan <- &entity.GameAct{
ActId: ki.actId,
Uid: ki.uid,
Action: actionData,
})
}
}
tx, err := g.DB().Begin(ctx)
if err != nil {
g.Log().Error(ctx, err)
return err
}
if len(addItems) > 0 {
_, err = tx.Model(Name).Data(addItems).Batch(100).Insert()
if err != nil {
g.Log().Errorf(ctx, "批量新增失败: %v", err)
tx.Rollback()
return err
}
}
if len(updateItems) > 0 {
for _, item := range updateItems {
item.UpdatedAt = gtime.Now()
_, err = tx.Model(Name).Where(do.GameAct{
Uid: item.Uid,
ActId: item.ActId,
}).Data(item).Update()
if err != nil {
g.Log().Errorf(ctx, "批量更新失败: %v", err)
}
}
}
err = tx.Commit()
// tx, err := g.DB().Begin(ctx)
// if err != nil {
// g.Log().Error(ctx, err)
// return err
// }
// if len(addItems) > 0 {
// _, err = tx.Model(Name).Data(addItems).Batch(100).Insert()
// if err != nil {
// g.Log().Errorf(ctx, "批量新增失败: %v", err)
// tx.Rollback()
// return err
// }
// }
// if len(updateItems) > 0 {
// for _, item := range updateItems {
// item.UpdatedAt = gtime.Now()
// _, err = tx.Model(Name).Where(do.GameAct{
// Uid: item.Uid,
// ActId: item.ActId,
// }).Data(item).Update()
// if err != nil {
// g.Log().Errorf(ctx, "批量更新失败: %v", err)
// }
// }
// }
// err = tx.Commit()
// if err != nil {
// g.Log().Errorf(ctx, "提交事务失败: %v", err)
// return err
// }
// for _, item := range addItems {
// s.DelCacheKey(ctx, item.ActId, item.Uid)
// }
// for _, item := range updateItems {
// s.DelCacheKey(ctx, item.ActId, item.Uid)
// }
// g.Log().Debugf(ctx, "SaveV2Batch完成: 新增%d, 更新%d", len(addItems), len(updateItems))
return nil
}
// SaveV2Batch 批量保存游戏活动数据 (优化版)
//
// @Description: 使用批量Redis MGET和批量数据库操作提升性能
// @param ctx context.Context: 上下文对象
// @param cacheKeys []string: 缓存键列表
// @return err error: 返回错误信息
func (s *sGameAct) SaveV3Batch(ctx context.Context, cacheKeys []string, dataChan chan *entity.GameAct) (err error) {
if len(cacheKeys) == 0 {
return nil
}
type keyInfo struct {
cacheKey string
actId int
uid int64
}
var keyInfos []keyInfo
activeUids := make(map[int64]bool)
//提取出不活跃的信息
for _, cacheKey := range cacheKeys {
result := strings.Split(cacheKey, ":")
if len(result) < 3 {
continue
}
actId := gconv.Int(result[1])
if actId == 0 {
continue
}
uid := gconv.Int64(result[2])
if uid == 0 {
continue
}
if getBool, _ := pkg.Cache("redis").Contains(ctx, fmt.Sprintf("act:update:%d", uid)); getBool {
activeUids[uid] = true
continue
}
keyInfos = append(keyInfos, keyInfo{
cacheKey: cacheKey,
actId: actId,
uid: uid,
})
}
if len(keyInfos) == 0 {
return nil
}
redisValues, err := g.Redis().MGet(ctx, cacheKeys...)
if err != nil {
g.Log().Errorf(ctx, "提交事务失败: %v", err)
g.Log().Errorf(ctx, "批量获取Redis失败: %v", err)
return err
}
for _, item := range addItems {
s.DelCacheKey(ctx, item.ActId, item.Uid)
}
for _, item := range updateItems {
s.DelCacheKey(ctx, item.ActId, item.Uid)
var validKeyInfos []keyInfo
for i, keyInfo := range keyInfos {
if val, ok := redisValues[keyInfo.cacheKey]; ok && gconv.String(val) != "" {
validKeyInfos = append(validKeyInfos, keyInfos[i])
}
}
g.Log().Debugf(ctx, "SaveV2Batch完成: 新增%d, 更新%d", len(addItems), len(updateItems))
if len(validKeyInfos) == 0 {
return nil
}
var uids []int64
for _, ki := range validKeyInfos {
if !tools.InArray[int64](uids, ki.uid) {
uids = append(uids, ki.uid)
}
}
var existingData []do.GameAct
err = g.Model(Name).Where("uid IN (?)", uids).Fields("uid,act_id").Scan(&existingData)
if err != nil {
g.Log().Errorf(ctx, "批量查询数据库失败: %v", err)
return err
}
for _, ki := range validKeyInfos {
val, ok := redisValues[ki.cacheKey]
if !ok {
continue
}
actionData := gconv.String(val)
if actionData == "" {
continue
}
dataChan <- &entity.GameAct{
ActId: ki.actId,
Uid: ki.uid,
Action: actionData,
}
}
return nil
}
@@ -783,7 +942,6 @@ func (s *sGameAct) DelCacheKey(ctx context.Context, aid int, uid int64) {
g.Log().Error(ctx, err)
}
}
}
// 清空GetRedDot缓存
func (s *sGameAct) RefreshGetRedDotCache(uid int64) {
@@ -814,3 +972,261 @@ func (s *sGameAct) Del(uid int64, actId int) {
}).Delete()
}
func (s *sGameAct) Cache2SqlChanOptimized(ctx context.Context, addChan, updateChan chan *entity.GameAct) {
const batchSize = 200
const commitThreshold = 1000 // 每 5000 条提交一次事务
var addBatch []*entity.GameAct
// var addDelBatch []*entity.GameAct
// var updateDelBatch []*entity.GameAct
var addAllCount, updateAllCount int64
var txAddCount, txUpdateCount int64 // 当前事务累计计数
tx, err := g.DB().Begin(ctx)
if err != nil {
g.Log().Errorf(ctx, "开启事务失败:%v", err)
return
}
// 刷新新增数据
flushAdd := func() {
if len(addBatch) == 0 {
return
}
addRes, err := tx.Model(Name).Data(addBatch).Batch(50).Insert()
if err != nil {
g.Log().Errorf(ctx, "批量新增失败:%v", err)
return
} else {
row, _ := addRes.RowsAffected()
if row != int64(len(addBatch)) {
//g.Log().Error(ctx, "本次未能全部新增,新增数据失败: %v", v)
return
}
txAddCount += row
}
for _, v := range addBatch {
s.DelCacheKey(ctx, v.ActId, v.Uid)
// addDelBatch = append(addDelBatch, addBatch...)
}
// for _, v := range addBatch {
// v.UpdatedAt = gtime.Now()
// addRes, err := tx.Model(Name).Data(v).Insert()
// if err != nil {
// g.Log().Errorf(ctx, "新增失败:%v", err)
// continue
// } else {
// row, _ := addRes.RowsAffected()
// if row == 0 {
// //g.Log().Error(ctx, "本次修改,数据失败: %v", v)
// continue
// }
// s.DelCacheKey(ctx, v.ActId, v.Uid)
// txAddCount += row
// addDelBatch = append(addDelBatch, v)
// }
// }
// 达到阈值提交事务
if txAddCount+txUpdateCount >= commitThreshold {
err := tx.Commit()
if err != nil {
g.Log().Errorf(ctx, "提交事务失败:%v", err)
return
}
addAllCount += txAddCount
updateAllCount += txUpdateCount
txUpdateCount = 0
txAddCount = 0
tx, _ = g.DB().Begin(ctx)
}
addBatch = addBatch[:0]
}
// 刷新更新数据
flushUpdate := func(data *entity.GameAct) {
data.UpdatedAt = gtime.Now()
updateRes, err := tx.Model(Name).Where(do.GameAct{
Uid: data.Uid,
ActId: data.ActId,
}).Data(data).Update()
if err != nil {
g.Log().Errorf(ctx, "更新失败:%v", err)
return
} else {
row, _ := updateRes.RowsAffected()
if row == 0 {
//g.Log().Error(ctx, "本次修改,数据失败: %v", v)
return
}
s.DelCacheKey(ctx, data.ActId, data.Uid)
txUpdateCount += row
// updateDelBatch = append(updateDelBatch, data)
}
// 达到阈值提交事务
if txUpdateCount+txAddCount >= commitThreshold {
err := tx.Commit()
if err != nil {
g.Log().Errorf(ctx, "提交事务失败:%v", err)
return
}
addAllCount += txAddCount
updateAllCount += txUpdateCount
txUpdateCount = 0
txAddCount = 0
tx, _ = g.DB().Begin(ctx)
}
}
addClosed := false
updateClosed := false
for {
if addClosed && updateClosed {
break
}
select {
case v, ok := <-addChan:
if !ok {
addClosed = true
continue
}
addBatch = append(addBatch, v)
if len(addBatch) >= batchSize {
flushAdd()
}
case v, ok := <-updateChan:
if !ok {
updateClosed = true
continue
}
flushUpdate(v)
case <-ctx.Done():
g.Log().Debug(ctx, "act协程被上下文取消")
return
}
}
// 处理剩余数据
flushAdd()
// 提交最后的事务
// if txAddCount+txUpdateCount > 0 {
err = tx.Commit()
if err != nil {
g.Log().Errorf(ctx, "提交事务失败:%v", err)
return
} else {
// for _, v := range addDelBatch {
// s.DelCacheKey(ctx, v.ActId, v.Uid)
// }
// for _, v := range updateDelBatch {
// s.DelCacheKey(ctx, v.ActId, v.Uid)
// }
addAllCount += txAddCount
updateAllCount += txUpdateCount
}
g.Log().Debugf(ctx, "运行结束act当前写入数据库: %v 条", addAllCount)
g.Log().Debugf(ctx, "运行结束act当前更新数据库: %v 条", updateAllCount)
}
func (s *sGameAct) Cache2SqlChanAll(ctx context.Context, dataChan chan *entity.GameAct) {
const commitThreshold = 5000 // 每 5000 条提交一次事务
var addAllCount, updateAllCount int64
var txAddCount, txUpdateCount int64 // 当前事务累计计数
tx, err := g.DB().Begin(ctx)
if err != nil {
g.Log().Errorf(ctx, "开启事务失败:%v", err)
return
}
// 刷新新增数据
flush := func(data *entity.GameAct) {
dataRes, err := tx.Model(Name).Data(data).Save()
if err != nil {
g.Log().Errorf(ctx, "保存失败:%v", err)
return
} else {
row, _ := dataRes.RowsAffected()
switch row {
case 0:
g.Log().Error(ctx, "本次未能保存,保存数据失败")
return
case 1:
//新增
txAddCount++
case 2:
//更新
txUpdateCount++
default:
g.Log().Errorf(ctx, "执行行数不对: %v", row)
}
s.DelCacheKey(ctx, data.ActId, data.Uid)
}
// 达到阈值提交事务
if txAddCount+txUpdateCount >= commitThreshold {
err := tx.Commit()
if err != nil {
g.Log().Errorf(ctx, "提交事务失败:%v", err)
return
}
addAllCount += txAddCount
updateAllCount += txUpdateCount
txAddCount = 0
txUpdateCount = 0
tx, _ = g.DB().Begin(ctx)
}
}
dataClosed := false
for {
if dataClosed {
break
}
select {
case v, ok := <-dataChan:
if !ok {
dataClosed = true
continue
}
flush(v)
case <-ctx.Done():
g.Log().Debug(ctx, "act协程被上下文取消")
return
}
}
// 提交最后的事务
// if txAddCount+txUpdateCount > 0 {
err = tx.Commit()
if err != nil {
g.Log().Errorf(ctx, "提交事务失败:%v", err)
return
} else {
// for _, v := range addDelBatch {
// s.DelCacheKey(ctx, v.ActId, v.Uid)
// }
// for _, v := range updateDelBatch {
// s.DelCacheKey(ctx, v.ActId, v.Uid)
// }
addAllCount += txAddCount
updateAllCount += txUpdateCount
}
g.Log().Debugf(ctx, "运行结束act当前写入数据库: %v 条", addAllCount)
g.Log().Debugf(ctx, "运行结束act当前更新数据库: %v 条", updateAllCount)
}

View File

@@ -55,6 +55,13 @@ type (
// @return err error: 返回错误信息
// SavesV2 保存游戏活动数据
SavesV2() (err error)
// SavesV3 保存游戏活动数据
//
// @Description: 保存游戏活动数据里面的超时时间时50分钟
// @receiver s *sGameAct: 游戏活动服务结构体指针
// @return err error: 返回错误信息
// SavesV3 保存游戏活动数据
SavesV3() (err error)
// SaveV2 保存游戏活动数据
//
// @Description: 保存游戏活动数据
@@ -76,12 +83,26 @@ type (
// Cache2AddChan 批量添加数据库
Cache2SqlChan(ctx context.Context, addChan chan *entity.GameAct, updateChan chan *entity.GameAct)
// SaveV2Batch 批量保存游戏活动数据 (优化版)
SaveV2Batch(ctx context.Context, cacheKeys []string) (err error)
//
// @Description: 使用批量Redis MGET和批量数据库操作提升性能
// @param ctx context.Context: 上下文对象
// @param cacheKeys []string: 缓存键列表
// @return err error: 返回错误信息
SaveV2Batch(ctx context.Context, cacheKeys []string, addChan chan *entity.GameAct, updateChan chan *entity.GameAct) (err error)
// SaveV2Batch 批量保存游戏活动数据 (优化版)
//
// @Description: 使用批量Redis MGET和批量数据库操作提升性能
// @param ctx context.Context: 上下文对象
// @param cacheKeys []string: 缓存键列表
// @return err error: 返回错误信息
SaveV3Batch(ctx context.Context, cacheKeys []string, dataChan chan *entity.GameAct) (err error)
// 删除缓存key
DelCacheKey(ctx context.Context, aid int, uid int64)
// 清空GetRedDot缓存
RefreshGetRedDotCache(uid int64)
Del(uid int64, actId int)
Cache2SqlChanOptimized(ctx context.Context, addChan chan *entity.GameAct, updateChan chan *entity.GameAct)
Cache2SqlChanAll(ctx context.Context, dataChan chan *entity.GameAct)
}
)

View File

@@ -17,10 +17,15 @@ type (
// @receiver s: sGameKv的实例。
// @return err: 错误信息如果操作成功则为nil。
SavesV1() (err error)
// SavesV2Batch 批量保存游戏KV数据 (优化版)
SavesV2Batch(ctx context.Context, cacheKeys []string) (err error)
// 删除缓存key
DelCacheKey(ctx context.Context, uid int64)
// SavesV2Batch 批量保存游戏KV数据 (优化版)
//
// @Description: 使用批量Redis MGET和批量数据库操作提升性能
// @param ctx context.Context: 上下文对象
// @param cacheKeys []string: 缓存键列表
// @return err error: 返回错误信息
SavesV2Batch(ctx context.Context, cacheKeys []string) (err error)
}
)