Tiny RDM 项目中跨数据库键显示异常问题分析
【免费下载链接】tiny-rdm A Modern Redis GUI Client 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-rdm
问题背景
Tiny RDM 是一个现代化的轻量级跨平台 Redis 桌面管理器,支持 Mac、Windows 和 Linux。在实际使用过程中,用户可能会遇到跨数据库键显示异常的问题,主要表现为:
- 在不同数据库之间切换时,键列表显示不正确
- 数据库切换后,键的统计信息未及时更新
- 集群模式下数据库键数量统计异常
技术架构分析
后端架构
Tiny RDM 采用 Go 语言作为后端,使用 Wails 框架构建桌面应用。后端服务主要包含以下几个核心组件:
前端架构
前端采用 Vue.js + Pinia 状态管理,浏览器树组件负责键的显示和管理:
跨数据库键显示异常问题分析
1. 数据库切换机制问题
在 browser_service.go 的 getRedisClient 方法中,存在数据库切换的逻辑:
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)
}
// 重新创建连接...
}
问题分析:当切换数据库时,会完全关闭现有连接并重新创建,这可能导致:
- 游标信息丢失:每个数据库的扫描游标(
cursor)存储在连接项中,切换时被清除 - 状态不一致:前端状态与后端实际连接状态可能不同步
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. 改进数据库切换机制
// 改进的数据库切换逻辑
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 项目中的跨数据库键显示异常问题主要源于:
- 数据库切换机制不完善:完全重建连接导致状态丢失
- 游标管理策略缺陷:缺乏按数据库的独立状态管理
- 前后端状态同步问题:切换时状态清理和恢复不彻底
通过改进数据库切换机制、增强游标管理、优化状态同步策略,可以有效解决这些问题。同时,引入连接池和状态缓存可以进一步提升性能和用户体验。
对于 Redis 桌面管理器这类工具软件,良好的状态管理和用户体验至关重要。Tiny RDM 作为一个现代化项目,通过持续优化这些问题,可以为用户提供更加稳定和高效的数据管理体验。
【免费下载链接】tiny-rdm A Modern Redis GUI Client 项目地址: https://gitcode.com/GitHub_Trending/ti/tiny-rdm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



