修复reids
This commit is contained in:
287
modules/redis_types.py
Normal file
287
modules/redis_types.py
Normal file
@@ -0,0 +1,287 @@
|
||||
"""
|
||||
Redis数据类型支持增强模块
|
||||
支持string、hash、list、set、zset、json等数据类型的比较
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from redis.exceptions import RedisError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_redis_value_with_type(redis_client, key):
|
||||
"""
|
||||
获取Redis键值及其数据类型
|
||||
|
||||
Args:
|
||||
redis_client: Redis客户端
|
||||
key: Redis键名
|
||||
|
||||
Returns:
|
||||
dict: {
|
||||
'type': 数据类型,
|
||||
'value': 值,
|
||||
'display_value': 用于显示的格式化值,
|
||||
'exists': 是否存在
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 检查key是否存在
|
||||
if not redis_client.exists(key):
|
||||
return {
|
||||
'type': None,
|
||||
'value': None,
|
||||
'display_value': None,
|
||||
'exists': False
|
||||
}
|
||||
|
||||
# 获取数据类型
|
||||
key_type = redis_client.type(key).decode('utf-8')
|
||||
result = {
|
||||
'type': key_type,
|
||||
'exists': True
|
||||
}
|
||||
|
||||
if key_type == 'string':
|
||||
# 字符串类型
|
||||
value = redis_client.get(key)
|
||||
if value:
|
||||
try:
|
||||
# 尝试解码为字符串
|
||||
str_value = value.decode('utf-8')
|
||||
result['value'] = str_value
|
||||
|
||||
# 尝试解析为JSON
|
||||
try:
|
||||
json_value = json.loads(str_value)
|
||||
result['display_value'] = json.dumps(json_value, indent=2, ensure_ascii=False)
|
||||
result['type'] = 'json_string' # 标记为JSON字符串
|
||||
except:
|
||||
result['display_value'] = str_value
|
||||
|
||||
except UnicodeDecodeError:
|
||||
# 二进制数据
|
||||
result['value'] = value
|
||||
result['display_value'] = f"<binary data: {len(value)} bytes>"
|
||||
else:
|
||||
result['value'] = ""
|
||||
result['display_value'] = ""
|
||||
|
||||
elif key_type == 'hash':
|
||||
# Hash类型
|
||||
hash_data = redis_client.hgetall(key)
|
||||
decoded_hash = {}
|
||||
|
||||
for field, value in hash_data.items():
|
||||
try:
|
||||
decoded_field = field.decode('utf-8')
|
||||
decoded_value = value.decode('utf-8')
|
||||
decoded_hash[decoded_field] = decoded_value
|
||||
except UnicodeDecodeError:
|
||||
decoded_hash[str(field)] = f"<binary: {len(value)} bytes>"
|
||||
|
||||
result['value'] = decoded_hash
|
||||
result['display_value'] = json.dumps(decoded_hash, indent=2, ensure_ascii=False)
|
||||
|
||||
elif key_type == 'list':
|
||||
# List类型
|
||||
list_data = redis_client.lrange(key, 0, -1)
|
||||
decoded_list = []
|
||||
|
||||
for item in list_data:
|
||||
try:
|
||||
decoded_item = item.decode('utf-8')
|
||||
decoded_list.append(decoded_item)
|
||||
except UnicodeDecodeError:
|
||||
decoded_list.append(f"<binary: {len(item)} bytes>")
|
||||
|
||||
result['value'] = decoded_list
|
||||
result['display_value'] = json.dumps(decoded_list, indent=2, ensure_ascii=False)
|
||||
|
||||
elif key_type == 'set':
|
||||
# Set类型
|
||||
set_data = redis_client.smembers(key)
|
||||
decoded_set = []
|
||||
|
||||
for item in set_data:
|
||||
try:
|
||||
decoded_item = item.decode('utf-8')
|
||||
decoded_set.append(decoded_item)
|
||||
except UnicodeDecodeError:
|
||||
decoded_set.append(f"<binary: {len(item)} bytes>")
|
||||
|
||||
# 排序以便比较
|
||||
decoded_set.sort()
|
||||
result['value'] = decoded_set
|
||||
result['display_value'] = json.dumps(decoded_set, indent=2, ensure_ascii=False)
|
||||
|
||||
elif key_type == 'zset':
|
||||
# Sorted Set类型
|
||||
zset_data = redis_client.zrange(key, 0, -1, withscores=True)
|
||||
decoded_zset = []
|
||||
|
||||
for member, score in zset_data:
|
||||
try:
|
||||
decoded_member = member.decode('utf-8')
|
||||
decoded_zset.append([decoded_member, score])
|
||||
except UnicodeDecodeError:
|
||||
decoded_zset.append([f"<binary: {len(member)} bytes>", score])
|
||||
|
||||
result['value'] = decoded_zset
|
||||
result['display_value'] = json.dumps(decoded_zset, indent=2, ensure_ascii=False)
|
||||
|
||||
else:
|
||||
# 未知类型
|
||||
result['value'] = f"<unsupported type: {key_type}>"
|
||||
result['display_value'] = f"<unsupported type: {key_type}>"
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取Redis键值失败 {key}: {e}")
|
||||
return {
|
||||
'type': 'error',
|
||||
'value': None,
|
||||
'display_value': f"<error: {str(e)}>",
|
||||
'exists': False
|
||||
}
|
||||
|
||||
def compare_redis_values(value1_info, value2_info):
|
||||
"""
|
||||
比较两个Redis值
|
||||
|
||||
Args:
|
||||
value1_info: 第一个值的信息字典
|
||||
value2_info: 第二个值的信息字典
|
||||
|
||||
Returns:
|
||||
dict: 比较结果
|
||||
"""
|
||||
# 检查存在性
|
||||
if not value1_info['exists'] and not value2_info['exists']:
|
||||
return {
|
||||
'status': 'both_missing',
|
||||
'message': '两个集群都不存在此键'
|
||||
}
|
||||
elif not value1_info['exists']:
|
||||
return {
|
||||
'status': 'missing_in_cluster1',
|
||||
'message': '集群1中不存在此键'
|
||||
}
|
||||
elif not value2_info['exists']:
|
||||
return {
|
||||
'status': 'missing_in_cluster2',
|
||||
'message': '集群2中不存在此键'
|
||||
}
|
||||
|
||||
# 检查类型
|
||||
type1 = value1_info['type']
|
||||
type2 = value2_info['type']
|
||||
|
||||
if type1 != type2:
|
||||
return {
|
||||
'status': 'different',
|
||||
'message': f'数据类型不同: {type1} vs {type2}'
|
||||
}
|
||||
|
||||
# 比较值
|
||||
value1 = value1_info['value']
|
||||
value2 = value2_info['value']
|
||||
|
||||
if type1 in ['string', 'json_string']:
|
||||
# 字符串比较
|
||||
if value1 == value2:
|
||||
return {'status': 'identical', 'message': '值相同'}
|
||||
else:
|
||||
return {'status': 'different', 'message': '值不同'}
|
||||
|
||||
elif type1 == 'hash':
|
||||
# Hash比较
|
||||
if value1 == value2:
|
||||
return {'status': 'identical', 'message': '哈希值相同'}
|
||||
else:
|
||||
# 详细比较哈希字段
|
||||
keys1 = set(value1.keys())
|
||||
keys2 = set(value2.keys())
|
||||
|
||||
if keys1 != keys2:
|
||||
return {'status': 'different', 'message': f'哈希字段不同: {keys1 - keys2} vs {keys2 - keys1}'}
|
||||
|
||||
diff_fields = []
|
||||
for key in keys1:
|
||||
if value1[key] != value2[key]:
|
||||
diff_fields.append(key)
|
||||
|
||||
if diff_fields:
|
||||
return {'status': 'different', 'message': f'哈希字段值不同: {diff_fields}'}
|
||||
else:
|
||||
return {'status': 'identical', 'message': '哈希值相同'}
|
||||
|
||||
elif type1 == 'list':
|
||||
# List比较(顺序敏感)
|
||||
if value1 == value2:
|
||||
return {'status': 'identical', 'message': '列表相同'}
|
||||
else:
|
||||
return {'status': 'different', 'message': f'列表不同,长度: {len(value1)} vs {len(value2)}'}
|
||||
|
||||
elif type1 == 'set':
|
||||
# Set比较(顺序无关)
|
||||
if set(value1) == set(value2):
|
||||
return {'status': 'identical', 'message': '集合相同'}
|
||||
else:
|
||||
return {'status': 'different', 'message': f'集合不同,大小: {len(value1)} vs {len(value2)}'}
|
||||
|
||||
elif type1 == 'zset':
|
||||
# Sorted Set比较
|
||||
if value1 == value2:
|
||||
return {'status': 'identical', 'message': '有序集合相同'}
|
||||
else:
|
||||
return {'status': 'different', 'message': f'有序集合不同,大小: {len(value1)} vs {len(value2)}'}
|
||||
|
||||
else:
|
||||
# 其他类型的通用比较
|
||||
if value1 == value2:
|
||||
return {'status': 'identical', 'message': '值相同'}
|
||||
else:
|
||||
return {'status': 'different', 'message': '值不同'}
|
||||
|
||||
def batch_get_redis_values_with_type(redis_client, keys, cluster_name="Redis集群", performance_tracker=None):
|
||||
"""
|
||||
批量获取Redis键值及类型信息
|
||||
|
||||
Args:
|
||||
redis_client: Redis客户端
|
||||
keys: 键名列表
|
||||
cluster_name: 集群名称
|
||||
performance_tracker: 性能追踪器
|
||||
|
||||
Returns:
|
||||
list: 每个键的值信息字典列表
|
||||
"""
|
||||
import time
|
||||
|
||||
start_time = time.time()
|
||||
results = []
|
||||
|
||||
logger.info(f"开始从{cluster_name}批量获取 {len(keys)} 个键的详细信息")
|
||||
|
||||
try:
|
||||
for key in keys:
|
||||
key_info = get_redis_value_with_type(redis_client, key)
|
||||
results.append(key_info)
|
||||
|
||||
end_time = time.time()
|
||||
duration = end_time - start_time
|
||||
|
||||
if performance_tracker:
|
||||
performance_tracker.record_query(f"{cluster_name}_detailed_query", duration)
|
||||
|
||||
successful_count = sum(1 for r in results if r['exists'])
|
||||
logger.info(f"从{cluster_name}详细查询完成,成功获取 {successful_count}/{len(keys)} 个值,耗时 {duration:.3f} 秒")
|
||||
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"从{cluster_name}批量详细查询失败: {e}")
|
||||
# 返回错误占位符
|
||||
return [{'type': 'error', 'value': None, 'display_value': f'<error: {e}>', 'exists': False} for _ in keys]
|
696
templates/redis_compare_old.html
Normal file
696
templates/redis_compare_old.html
Normal file
@@ -0,0 +1,696 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Redis集群比对工具</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.config-section {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.result-section {
|
||||
margin-top: 30px;
|
||||
}
|
||||
.difference-item {
|
||||
border-left: 4px solid #dc3545;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #fff5f5;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.identical-item {
|
||||
border-left: 4px solid #28a745;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #f8fff8;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.missing-item {
|
||||
border-left: 4px solid #ffc107;
|
||||
padding-left: 15px;
|
||||
margin-bottom: 15px;
|
||||
background-color: #fffbf0;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.stat-card {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
.query-keys {
|
||||
min-height: 120px;
|
||||
}
|
||||
.redis-value {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
background-color: #f8f9fa !important;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
.node-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.node-input input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.performance-section {
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
.log-viewer {
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9em;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.navbar {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
|
||||
}
|
||||
.breadcrumb {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
.breadcrumb-item a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn-redis {
|
||||
background: linear-gradient(135deg, #dc143c 0%, #b91c1c 100%);
|
||||
border: none;
|
||||
color: white;
|
||||
}
|
||||
.btn-redis:hover {
|
||||
background: linear-gradient(135deg, #b91c1c 0%, #991b1b 100%);
|
||||
color: white;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.redis-logo {
|
||||
color: #dc143c;
|
||||
}
|
||||
.cluster-config {
|
||||
border: 2px solid #e9ecef;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.cluster-config.active {
|
||||
border-color: #dc143c;
|
||||
background-color: #fff8f8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">
|
||||
<i class="fas fa-database me-2"></i>
|
||||
大数据工具集合
|
||||
</a>
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a class="nav-link" href="/">
|
||||
<i class="fas fa-home"></i> 首页
|
||||
</a>
|
||||
<a class="nav-link" href="/db-compare">
|
||||
<i class="fas fa-exchange-alt"></i> 数据库比对
|
||||
</a>
|
||||
<a class="nav-link active" href="/redis-compare">
|
||||
<i class="fab fa-redis"></i> Redis比对
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container mt-4">
|
||||
<!-- 面包屑导航 -->
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/">首页</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Redis集群比对工具</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- 页面标题 -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fab fa-redis redis-logo fs-1 me-3"></i>
|
||||
<div>
|
||||
<h1 class="mb-1">Redis集群比对工具</h1>
|
||||
<p class="text-muted mb-0">专业的Redis集群数据比对工具,支持随机采样和指定Key查询</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 配置区域 -->
|
||||
<div class="row">
|
||||
<!-- 配置管理 -->
|
||||
<div class="col-lg-12">
|
||||
<div class="config-section mb-4">
|
||||
<h4><i class="fas fa-cogs me-2"></i>配置管理</h4>
|
||||
|
||||
<div class="row">
|
||||
<!-- 配置组管理 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-layer-group me-2"></i>配置组管理</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-8">
|
||||
<select class="form-select form-select-sm" id="redisConfigGroupSelect">
|
||||
<option value="">选择Redis配置组...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button class="btn btn-primary btn-sm w-100" onclick="loadSelectedRedisConfigGroup()">
|
||||
<i class="fas fa-download"></i> 加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<button class="btn btn-success btn-sm w-100" onclick="showSaveRedisConfigDialog()">
|
||||
<i class="fas fa-save"></i> 保存配置
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<button class="btn btn-info btn-sm w-100" onclick="showManageRedisConfigDialog()">
|
||||
<i class="fas fa-cog"></i> 管理配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查询历史 -->
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-history me-2"></i>查询历史</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-8">
|
||||
<select class="form-select form-select-sm" id="redisHistorySelect">
|
||||
<option value="">选择历史记录...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<button class="btn btn-warning btn-sm w-100" onclick="loadSelectedRedisHistory()">
|
||||
<i class="fas fa-history"></i> 加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<button class="btn btn-secondary btn-sm w-100" onclick="showSaveRedisHistoryDialog()">
|
||||
<i class="fas fa-bookmark"></i> 保存历史
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<button class="btn btn-info btn-sm w-100" onclick="showManageRedisHistoryDialog()">
|
||||
<i class="fas fa-list"></i> 管理历史
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一键导入区域 -->
|
||||
<div class="row mt-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-download me-2"></i>配置导入</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button class="btn btn-outline-primary btn-sm me-2" onclick="showImportRedisConfigDialog('cluster1')">
|
||||
<i class="fas fa-download"></i> 导入集群1配置
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="showImportRedisConfigDialog('cluster2')">
|
||||
<i class="fas fa-download"></i> 导入集群2配置
|
||||
</button>
|
||||
<small class="form-text text-muted d-block mt-2">
|
||||
支持YAML格式配置导入,如:clusterName、clusterAddress、clusterPassword等
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h6><i class="fas fa-file-alt me-2"></i>查询日志</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<button class="btn btn-outline-info btn-sm" onclick="showRedisQueryLogsDialog()">
|
||||
<i class="fas fa-eye"></i> 查看查询日志
|
||||
</button>
|
||||
<small class="form-text text-muted d-block mt-2">
|
||||
查看Redis比较操作的详细执行日志
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis集群配置 -->
|
||||
<div class="col-lg-12">
|
||||
<div class="config-section">
|
||||
<h4><i class="fas fa-server me-2"></i>Redis集群配置</h4>
|
||||
|
||||
<div class="row">
|
||||
<!-- 第一个集群配置 -->
|
||||
<div class="col-md-6">
|
||||
<div class="cluster-config" id="cluster1-config">
|
||||
<h5><i class="fas fa-database me-2"></i>集群1 (生产环境)</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">集群名称</label>
|
||||
<input type="text" class="form-control" id="cluster1-name" value="生产集群" placeholder="输入集群名称">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">节点列表</label>
|
||||
<div id="cluster1-nodes">
|
||||
<div class="node-input">
|
||||
<input type="text" class="form-control node-host" placeholder="主机地址" value="127.0.0.1">
|
||||
<input type="number" class="form-control node-port" placeholder="端口" value="7000" style="width: 100px;">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-node">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addNode('cluster1')">
|
||||
<i class="fas fa-plus"></i> 添加节点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">密码 (可选)</label>
|
||||
<input type="password" class="form-control" id="cluster1-password" placeholder="Redis密码">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label class="form-label">连接超时 (秒)</label>
|
||||
<input type="number" class="form-control" id="cluster1-timeout" value="3" min="1" max="30">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">最大连接数</label>
|
||||
<input type="number" class="form-control" id="cluster1-max-conn" value="16" min="1" max="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-outline-success btn-sm" onclick="testConnection('cluster1')">
|
||||
<i class="fas fa-plug"></i> 测试连接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 第二个集群配置 -->
|
||||
<div class="col-md-6">
|
||||
<div class="cluster-config" id="cluster2-config">
|
||||
<h5><i class="fas fa-database me-2"></i>集群2 (测试环境)</h5>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">集群名称</label>
|
||||
<input type="text" class="form-control" id="cluster2-name" value="测试集群" placeholder="输入集群名称">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">节点列表</label>
|
||||
<div id="cluster2-nodes">
|
||||
<div class="node-input">
|
||||
<input type="text" class="form-control node-host" placeholder="主机地址" value="127.0.0.1">
|
||||
<input type="number" class="form-control node-port" placeholder="端口" value="7001" style="width: 100px;">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm remove-node">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-2" onclick="addNode('cluster2')">
|
||||
<i class="fas fa-plus"></i> 添加节点
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">密码 (可选)</label>
|
||||
<input type="password" class="form-control" id="cluster2-password" placeholder="Redis密码">
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-6">
|
||||
<label class="form-label">连接超时 (秒)</label>
|
||||
<input type="number" class="form-control" id="cluster2-timeout" value="3" min="1" max="30">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">最大连接数</label>
|
||||
<input type="number" class="form-control" id="cluster2-max-conn" value="16" min="1" max="100">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<button type="button" class="btn btn-outline-success btn-sm" onclick="testConnection('cluster2')">
|
||||
<i class="fas fa-plug"></i> 测试连接
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查询选项 -->
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<div class="config-section">
|
||||
<h4><i class="fas fa-search me-2"></i>查询选项</h4>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">查询模式</label>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="queryMode" id="randomMode" value="random" checked>
|
||||
<label class="form-check-label" for="randomMode">
|
||||
<i class="fas fa-random me-1"></i>随机采样查询
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="radio" name="queryMode" id="specifiedMode" value="specified">
|
||||
<label class="form-check-label" for="specifiedMode">
|
||||
<i class="fas fa-list me-1"></i>指定Key查询
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 随机采样选项 -->
|
||||
<div id="randomOptions">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">采样数量</label>
|
||||
<input type="number" class="form-control" id="sampleCount" value="100" min="1" max="10000">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Key匹配模式</label>
|
||||
<input type="text" class="form-control" id="keyPattern" value="*" placeholder="例如: user:*">
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">数据源集群</label>
|
||||
<select class="form-select" id="sourceCluster">
|
||||
<option value="cluster1">集群1 (生产环境)</option>
|
||||
<option value="cluster2" selected>集群2 (测试环境)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 指定Key选项 -->
|
||||
<div id="specifiedOptions" style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">指定Key列表 (每行一个)</label>
|
||||
<textarea class="form-control query-keys" id="specifiedKeys"
|
||||
placeholder="请输入要查询的Key,每行一个 例如: user:1001 user:1002 session:abc123"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="text-center">
|
||||
<button type="button" class="btn btn-redis btn-lg me-3" onclick="executeRedisComparison()">
|
||||
<i class="fas fa-play me-2"></i>开始比较
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-lg" onclick="clearResults()">
|
||||
<i class="fas fa-eraser me-2"></i>清空结果
|
||||
</button>
|
||||
|
||||
<div class="loading mt-3">
|
||||
<div class="spinner-border text-danger me-2" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<span>正在执行Redis数据比较,请稍候...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果展示区域 -->
|
||||
<div class="result-section" id="results" style="display: none;">
|
||||
<!-- 统计卡片 -->
|
||||
<div class="row" id="statsCards">
|
||||
<!-- 统计卡片将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
|
||||
<!-- 详细结果 -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ul class="nav nav-tabs" id="resultTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="differences-tab" data-bs-toggle="tab" data-bs-target="#differences" type="button" role="tab">
|
||||
<i class="fas fa-exclamation-triangle me-1"></i>差异数据 (<span id="diff-count">0</span>)
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="identical-tab" data-bs-toggle="tab" data-bs-target="#identical" type="button" role="tab">
|
||||
<i class="fas fa-check-circle me-1"></i>相同数据 (<span id="identical-count">0</span>)
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="missing-tab" data-bs-toggle="tab" data-bs-target="#missing" type="button" role="tab">
|
||||
<i class="fas fa-question-circle me-1"></i>缺失数据 (<span id="missing-count">0</span>)
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="performance-tab" data-bs-toggle="tab" data-bs-target="#performance" type="button" role="tab">
|
||||
<i class="fas fa-chart-line me-1"></i>性能报告
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content" id="resultTabContent">
|
||||
<!-- 差异数据 -->
|
||||
<div class="tab-pane fade show active" id="differences" role="tabpanel">
|
||||
<div class="mt-3" id="differenceResults">
|
||||
<!-- 差异结果将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 相同数据 -->
|
||||
<div class="tab-pane fade" id="identical" role="tabpanel">
|
||||
<div class="mt-3" id="identicalResults">
|
||||
<!-- 相同结果将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 缺失数据 -->
|
||||
<div class="tab-pane fade" id="missing" role="tabpanel">
|
||||
<div class="mt-3" id="missingResults">
|
||||
<!-- 缺失结果将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 性能报告 -->
|
||||
<div class="tab-pane fade" id="performance" role="tabpanel">
|
||||
<div class="performance-section mt-3" id="performanceReport">
|
||||
<!-- 性能报告将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis配置组保存模态框 -->
|
||||
<div class="modal fade" id="saveRedisConfigModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-save me-2"></i>保存Redis配置组</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">配置组名称 *</label>
|
||||
<input type="text" class="form-control" id="redisConfigGroupName" placeholder="输入配置组名称">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea class="form-control" id="redisConfigGroupDescription" placeholder="输入配置组描述(可选)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-success" onclick="saveRedisConfigGroup()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis配置管理模态框 -->
|
||||
<div class="modal fade" id="manageRedisConfigModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-cog me-2"></i>Redis配置组管理</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="redisConfigGroupList">
|
||||
<!-- 配置组列表将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis配置导入模态框 -->
|
||||
<div class="modal fade" id="importRedisConfigModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-download me-2"></i>导入Redis配置</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
<strong>支持的配置格式:</strong><br>
|
||||
clusterName: "redis-test"<br>
|
||||
clusterAddress: "10.20.2.109:6470"<br>
|
||||
clusterPassword: ""<br>
|
||||
cachePrefix: message.status.Writer.<br>
|
||||
cacheTtl: 2000<br>
|
||||
async: true
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">配置内容</label>
|
||||
<textarea class="form-control" id="redisConfigImportText" rows="8"
|
||||
placeholder="请粘贴Redis配置内容..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-primary" onclick="importRedisConfig()">导入配置</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis查询历史保存模态框 -->
|
||||
<div class="modal fade" id="saveRedisHistoryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-bookmark me-2"></i>保存Redis查询历史</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">历史记录名称 *</label>
|
||||
<input type="text" class="form-control" id="redisHistoryName" placeholder="输入历史记录名称">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">描述</label>
|
||||
<textarea class="form-control" id="redisHistoryDescription" placeholder="输入历史记录描述(可选)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
|
||||
<button type="button" class="btn btn-success" onclick="saveRedisQueryHistory()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis查询历史管理模态框 -->
|
||||
<div class="modal fade" id="manageRedisHistoryModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-list me-2"></i>Redis查询历史管理</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="redisHistoryList">
|
||||
<!-- 查询历史列表将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Redis查询日志模态框 -->
|
||||
<div class="modal fade" id="redisQueryLogsModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-xl">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title"><i class="fas fa-file-alt me-2"></i>Redis查询日志</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h6 class="mb-0">Redis查询执行日志</h6>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" onclick="refreshRedisQueryLogs()">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="clearRedisQueryLogs()">
|
||||
<i class="fas fa-trash"></i> 清空
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="redisQueryLogs" class="log-viewer">
|
||||
<!-- 查询日志将在这里动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 引入必要的JavaScript -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/static/js/redis_compare.js"></script>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user