Tiny RDM 项目中跨数据库键显示异常问题分析

Tiny RDM 项目中跨数据库键显示异常问题分析

【免费下载链接】tiny-rdm A Modern Redis GUI Client 【免费下载链接】tiny-rdm 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-rdm

问题背景

Tiny RDM 是一个现代化的轻量级跨平台 Redis 桌面管理器,支持 Mac、Windows 和 Linux。在实际使用过程中,用户可能会遇到跨数据库键显示异常的问题,主要表现为:

  • 在不同数据库之间切换时,键列表显示不正确
  • 数据库切换后,键的统计信息未及时更新
  • 集群模式下数据库键数量统计异常

技术架构分析

后端架构

Tiny RDM 采用 Go 语言作为后端,使用 Wails 框架构建桌面应用。后端服务主要包含以下几个核心组件:

mermaid

前端架构

前端采用 Vue.js + Pinia 状态管理,浏览器树组件负责键的显示和管理:

mermaid

跨数据库键显示异常问题分析

1. 数据库切换机制问题

browser_service.gogetRedisClient 方法中,存在数据库切换的逻辑:

func (b *browserService) getRedisClient(server string, db int) (item *connectionItem, err error) {
    b.mutex.Lock()
    defer b.mutex.Unlock()

    if item, ok := b.connMap[server]; ok {
        if item.db == db || db < 0 {
            // 直接返回,不切换数据库
            return
        }
        // 关闭之前的连接
        if item.cancelFunc != nil {
            item.cancelFunc()
        }
        item.client.Close()
        delete(b.connMap, server)
    }
    // 重新创建连接...
}

问题分析:当切换数据库时,会完全关闭现有连接并重新创建,这可能导致:

  1. 游标信息丢失:每个数据库的扫描游标(cursor)存储在连接项中,切换时被清除
  2. 状态不一致:前端状态与后端实际连接状态可能不同步

2. 游标管理机制

type connectionItem struct {
    cursor      map[int]uint64      // 当前数据库的游标
    entryCursor map[int]entryCursor // 当前条目的游标
    stepSize    int64
    db          int // 当前数据库索引
}

问题分析:游标按数据库索引存储,但在集群模式下存在特殊处理:

// 集群模式下的特殊处理
if cluster, ok := client.(*redis.ClusterClient); ok {
    // FIXME: BUG? 集群模式下无法完全加载?可能需要移除共享的"游标"
    err = cluster.ForEachMaster(ctx, func(ctx context.Context, cli *redis.Client) error {
        return scan(ctx, cli, partCount, func(k []any) {
            mutex.Lock()
            keys = append(keys, k...)
            mutex.Unlock()
        })
    })
}

3. 前端状态同步问题

在前端的 browser.js store 中:

async openDatabase(server, db) {
    const { data, success, msg } = await OpenDatabase(server, db)
    if (!success) {
        throw new Error(msg)
    }
    const { keys = [], end = false, maxKeys = 0 } = data

    const serverInst = this.servers[server]
    if (serverInst == null) {
        return
    }
    serverInst.db = db
    serverInst.setDatabaseKeyCount(db, maxKeys)
    serverInst.loadingState.fullLoaded = end
    // ... 其他逻辑
}

问题分析:前端在切换数据库时,需要确保:

  1. 清空当前显示的键列表
  2. 更新数据库统计信息
  3. 重置加载状态

解决方案

1. 改进数据库切换机制

// 改进的数据库切换逻辑
func (b *browserService) switchDatabase(server string, newDB int) error {
    item, exists := b.connMap[server]
    if !exists {
        return errors.New("connection not found")
    }
    
    if item.db == newDB {
        return nil // 无需切换
    }
    
    // 保存当前数据库的游标状态
    currentCursor := item.cursor[item.db]
    
    // 执行 SELECT 命令切换数据库
    err := item.client.Do(item.ctx, "SELECT", newDB).Err()
    if err != nil {
        return err
    }
    
    // 更新状态
    item.db = newDB
    // 恢复新数据库的游标(如果存在)
    if savedCursor, exists := item.cursor[newDB]; exists {
        // 使用保存的游标
    } else {
        // 重置游标
        item.cursor[newDB] = 0
    }
    
    return nil
}

2. 增强游标管理

// 增强的游标管理结构
type databaseState struct {
    cursor      uint64
    scanPattern string
    keyType     string
    lastScan    time.Time
}

type connectionItem struct {
    client         redis.UniversalClient
    dbStates       map[int]*databaseState // 每个数据库的状态
    currentDB      int
    // ... 其他字段
}

3. 前端状态同步优化

// 改进的前端数据库切换逻辑
async switchDatabase(server, newDB) {
    const serverInst = this.servers[server]
    if (!serverInst) {
        throw new Error('Server not connected')
    }
    
    // 保存当前数据库的状态
    const currentDB = serverInst.db
    this.saveDatabaseState(server, currentDB)
    
    // 清空当前显示
    serverInst.clearDisplay()
    
    // 切换到新数据库
    await this.openDatabase(server, newDB)
    
    // 恢复新数据库的状态(如果存在)
    this.restoreDatabaseState(server, newDB)
}

测试方案

单元测试

func TestDatabaseSwitch(t *testing.T) {
    service := Browser()
    
    // 测试正常切换
    err := service.switchDatabase("test-server", 1)
    assert.NoError(t, err)
    
    // 测试游标保持
    service.setClientCursor("test-server", 1, 100)
    err = service.switchDatabase("test-server", 2)
    assert.NoError(t, err)
    
    // 切换回数据库1,游标应该保持
    err = service.switchDatabase("test-server", 1)
    assert.NoError(t, err)
    cursor := service.getClientCursor("test-server", 1)
    assert.Equal(t, uint64(100), cursor)
}

集成测试

// 前端集成测试
describe('Database Switching', () => {
    it('should correctly switch between databases', async () => {
        // 连接到测试服务器
        await browserStore.openConnection('test-server')
        
        // 在db0添加一些键
        await addTestKeys('test-server', 0, 10)
        
        // 切换到db1
        await browserStore.openDatabase('test-server', 1)
        
        // 验证显示已清空
        const keys = browserStore.getKeyStruct('test-server')
        expect(keys).toHaveLength(0)
        
        // 添加键到db1并验证显示
        await addTestKeys('test-server', 1, 5)
        await browserStore.loadMoreKeys('test-server', 1)
        const newKeys = browserStore.getKeyStruct('test-server')
        expect(newKeys).toHaveLength(5)
    })
})

性能优化建议

1. 连接池管理

// 实现连接池来避免频繁创建销毁连接
type connectionPool struct {
    pool      map[string]*redis.UniversalClient
    maxActive int
    mutex     sync.Mutex
}

func (p *connectionPool) getClient(server string, db int) (redis.UniversalClient, error) {
    p.mutex.Lock()
    defer p.mutex.Unlock()
    
    key := fmt.Sprintf("%s#%d", server, db)
    if client, exists := p.pool[key]; exists {
        return client, nil
    }
    
    // 创建新连接
    client, err := createNewConnection(server, db)
    if err != nil {
        return nil, err
    }
    
    if len(p.pool) >= p.maxActive {
        // 淘汰最久未使用的连接
        p.evictLRU()
    }
    
    p.pool[key] = &client
    return client, nil
}

2. 状态缓存策略

// 前端状态缓存
class DatabaseStateCache {
    constructor(maxSize = 10) {
        this.cache = new Map()
        this.maxSize = maxSize
        this.accessOrder = []
    }
    
    get(server, db) {
        const key = `${server}#${db}`
        if (this.cache.has(key)) {
            // 更新访问顺序
            this.accessOrder = this.accessOrder.filter(k => k !== key)
            this.accessOrder.push(key)
            return this.cache.get(key)
        }
        return null
    }
    
    set(server, db, state) {
        const key = `${server}#${db}`
        if (this.cache.size >= this.maxSize) {
            // 移除最久未使用的
            const lruKey = this.accessOrder.shift()
            if (lruKey) {
                this.cache.delete(lruKey)
            }
        }
        this.cache.set(key, state)
        this.accessOrder.push(key)
    }
}

总结

Tiny RDM 项目中的跨数据库键显示异常问题主要源于:

  1. 数据库切换机制不完善:完全重建连接导致状态丢失
  2. 游标管理策略缺陷:缺乏按数据库的独立状态管理
  3. 前后端状态同步问题:切换时状态清理和恢复不彻底

通过改进数据库切换机制、增强游标管理、优化状态同步策略,可以有效解决这些问题。同时,引入连接池和状态缓存可以进一步提升性能和用户体验。

对于 Redis 桌面管理器这类工具软件,良好的状态管理和用户体验至关重要。Tiny RDM 作为一个现代化项目,通过持续优化这些问题,可以为用户提供更加稳定和高效的数据管理体验。

【免费下载链接】tiny-rdm A Modern Redis GUI Client 【免费下载链接】tiny-rdm 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-rdm

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值