1. 背景

redis 被用做内存数据库,一些需要基础数据从数据库筛选出来,拼接成字符串放入 redis 的 sethash 结构中。

基础数据有生效、失效时间,现有是通过 quartz job 定时刷入、刷出。后来接到投诉,发现数据库和 redis 数据不一致现象,原因未知,因此需要做数据比对及数据同步。

2. 思路

2.1. 方案一

步骤:

  1. 筛选数据库数据到中间表A
  2. 根据入 redis 规则,将对应 key 中的数据导出并拆分入中间表B
  3. 编写SQL比对中间表A、B

优点: 统计结果应该比较直观,数据准备好,SQL比较2个表的差异还是很容易写出来
缺点: 数据库有几个字段涉及拆分,比如表示全国的值,要拆分很多个省份值,这个拆分工作以我的SQL水是搞不定的。
总结: 效率不好说,单就是一个数据库表就要建2个辅助中间表,这样的扩展性就不会太高,

2.2. 方案二

步骤:

  1. 筛选数据库数据导出到文件A
  2. 写脚本做数据拆分
  3. 根据入 redis 规则,将对应导出的文件转换成 pipe 数据文件
  4. 将数据文件导入 new_key
  5. 在事务内执行 redis 重命名:rename key old_key 及 rename new_key key
  6. 导出 redis 中 old_key 的数据到文件B
  7. sort、uniq、diff 比对A、B差异

优点: 数据同步这块工作最轻松,导入1000数据在57s左右,rename千万级key在0.013s左右;数据比对无需新建比对中间表;导出数据用spool,比对用sort、uniq、diff等,效率还是蛮高的。
缺点: 如果数据是同步的,就会做很多无用功;另外结果也不够直观,差异记录数等需要自己计算;new_key, key, old_key 在 redis 集群内必须被分到同一个solt上,可以去查阅相关资料深入了解。
其他: 我们的keykey名字设计最初没有考虑过rename等等情况,导致new_key, key, old_key 没法被分配到同一个solt上,方案也就没法用了。

2.3. 方案三

步骤:

  1. 筛选数据库数据导出到文件A
  2. 写脚本做数据拆分
  3. 导出 redis 中的数据到文件B
  4. sort、uniq、diff 比对A、B差异

优点: 数据同步较麻烦,要根据数据比对结果再一步加工,不过需要同步的数据量应该不多。数据比对的优点同方案二
缺点: 数据结果不够直观,不过自己写shell还是能算出来的。
其他: 数据比对,这个方案应该是最合适的了,简单。

3. 实现

接到任务花了点时间做验证。

然后把后两个方案的设计及可行性报告做出来反馈了。

再然后大家基本上都比较认同方案三,所以实现就按照这个来了。业务上我不太懂,sql请做这块数据同步的同事帮忙写的。

一开始就打算用shell完成,不说开发效率,单就运行环境这一项就不想考虑什么python、golang、java等等实现了,再说这个以后要交接出去,能简单的就不复杂化。

3.1. 脚本

按照方案来,考虑了下是和 redis 中的 key 做比对,那导出数据的 sql 就准备放在 ${key}.sql,数据拆分用 awk 来做,拆分文件放在 ${key}.awk

把下面的shell脚本串起来就是完整的脚本了

3.1.1. shell入参

1
2
3
4
if [ $# -lt 2 ]; then
echo "Usage: $0 user/pass@db key [redishostname:redisport]"
exit 1
fi

3.1.2. 参数初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
## SQLPLUS 数据库连接参数
DB_LINK=$1
REDIS_KEY=$2
REDIS_CONNECT=$3
REDIS_PORT=7000
REDIS_HOSTNAME=127.0.0.1
if [ "x${REDIS_CONNECT}" != "x" ]; then
REDIS_HOSTNAME=`echo ${REDIS_CONNECT} |cut -d ":" -f 1`
REDIS_PORT=`echo ${REDIS_CONNECT} |cut -d ":" -f 2`
fi
if [ "x${REDIS_HOSTNAME}" == "x" ]; then
REDIS_HOSTNAME=127.0.0.1
fi
if [ "x${REDIS_PORT}" == "x" ]; then
REDIS_PORT=7000
fi

3.1.3. 文件名设置

1
2
3
4
5
6
7
8
9
## 当需要多次比对并保留比对结果时,版本号可以用当前时间
VERSION=`date "+%s%N"`
DB_OUTPUT=db${VERSION}.output
DB_OUTPUT_SPLIT=${DB_OUTPUT}.split
DB_OUTPUT_SORT=${DB_OUTPUT}.sort
REDIS_OUTPUT=redis${VERSION}.output
REDIS_OUTPUT_SORT=${REDIS_OUTPUT}.sort
DIFF_OUTPUT=diff${VERSION}.output

3.1.4. 数据库数据导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
which sqlplus 2>&1
ECODE=$?
if [ $ECODE -ne 0 ]; then
echo "当前环境缺少 sqlplus 命令,脚本无法执行,请补充设置或拆分脚本至其他机器执行"
exit $ECODE
fi
echo "使用SQL文件 ${REDIS_KEY}.sql 导出数据"
## 参数参考:http://docs.oracle.com/cd/B19306_01/server.102/b14357/ch12040.htm#i2699269
sqlplus -s ${DB_LINK}<<EOF
SET COLSEP '_';
SET ECHO OFF;
SET FEEDBACK OFF;
SET TERMOUT OFF;
SET NEWPAGE NONE;
SET HEADING OFF;
SET SPACE 0;
SET PAGESIZE 0;
SET TRIMOUT ON;
SET TRIMSPOOL ON;
SET LINESIZE 2500;
SPOOL ${DB_OUTPUT}
@${REDIS_KEY}.sql;
EOF
ECODE=$?
if [ $ECODE -eq 0 ]; then
echo "成功导出数据库文件"
else
echo "导出数据库文件失败"
exit $ECODE
fi

3.1.5. redis 数据导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
which redis-cli 2>&1
ECODE=$?
if [ $ECODE -ne 0 ]; then
echo "当前环境缺少 sqlplus 命令,脚本无法执行,请补充设置或拆分脚本至其他机器执行"
exit $ECODE
fi
echo "使用REDIS命令 smembers $REDIS_KEY 导出数据"
redis-cli -c -h ${REDIS_HOSTNAME} -p ${REDIS_PORT} smembers $REDIS_KEY > ${REDIS_OUTPUT}
ECODE=$?
if [ $ECODE -eq 0 ]; then
echo "成功导出REDIS 文件"
else
echo "导出REDIS 文件失败"
exit $ECODE
fi

3.1.6. 数据库导出数据再处理

1
2
3
4
5
6
7
8
9
10
11
12
## 数据库数据需要再处理
SPLIT_FILE=${REDIS_KEY}.awk
if [ -f ${SPLIT_FILE} ]; then
echo "使用AWK文件 ${SPLIT_FILE} 做数据拆分"
awk -f ${SPLIT_FILE} ${DB_OUTPUT} > ${DB_OUTPUT_SPLIT}
fi
if [ -f ${DB_OUTPUT_SPLIT} ];then
echo "数据拆分完成"
else
cp ${DB_OUTPUT} ${DB_OUTPUT_SPLIT}
fi

3.1.7. 比对前准备

1
2
3
## 整理比对数据,排序去重
sort ${DB_OUTPUT_SPLIT} |uniq > ${DB_OUTPUT_SORT}
sort ${REDIS_OUTPUT} |uniq > ${REDIS_OUTPUT_SORT}

3.1.8. 比对数据

1
2
## 比对数据
diff -y --suppress-common-lines ${DB_OUTPUT_SORT} ${REDIS_OUTPUT_SORT} > ${DIFF_OUTPUT} 2>&1

3.1.9. 统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
## 统计行数,用于最后输出
DB_TOTAL=`wc -l ${DB_OUTPUT} |cut -d ' ' -f 1`
DB_SPLIT_TOTAL=`wc -l ${DB_OUTPUT_SPLIT} |cut -d ' ' -f 1`
DB_SORT_TOTAL=`wc -l ${DB_OUTPUT_SORT} |cut -d ' ' -f 1`
REDIS_TOTAL=`wc -l ${REDIS_OUTPUT} |cut -d ' ' -f 1`
REDIS_SORT_TOTAL=`wc -l ${REDIS_OUTPUT_SORT} |cut -d ' ' -f 1`
DIFF_TOTAL=`wc -l ${DIFF_OUTPUT} |cut -d ' ' -f 1`
DB_GT=`grep "<" ${DIFF_OUTPUT} |wc -l`
REDIS_GT=`grep ">" ${DIFF_OUTPUT} |wc -l`
## 同一行数据不一致
NEQ=`grep "|" ${DIFF_OUTPUT} |wc -l`
let EQ=${DB_SORT_TOTAL}-${DB_GT}
echo "=================================================="
echo "数据库生效数据总 ${DB_TOTAL} 条"
echo "数据库拆分后数据 ${DB_SPLIT_TOTAL} 条"
echo "数据库未重复数据 ${DB_SORT_TOTAL} 条"
echo "REDIS 生效数据总 ${REDIS_TOTAL} 条"
echo "REDIS 未重复数据 ${REDIS_SORT_TOTAL} 条"
echo "数据库REDIS 相同 ${EQ} 条"
echo "数据库比REDIS 多 ${DB_GT} 条"
echo "REDIS 比数据库多 ${REDIS_GT} 条"
echo "=================================================="

3.2. 导出SQL

写了个简单的示例,sql文件要放到 ${key}.sql

1
select '1_2_3_4_5_6_7' from dual;

3.3. AWK拆分文件

  1. 将省份是000的,拆分成所有省份;否则将省份字段直接按逗号分隔。
  2. 将产品线是0的,拆分产品0-4的5条
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BEGIN {
FS="_"
}
{
split("0,1,2,3,4", LINE, ",")
split($5, EFFECT_PROV, ",")
if ( $5 == "000" ) {
split("100,200,210,220,230,240,250,270,280,290,311,351,371,431,451,471,531,551,571,591,731,771,791,851,871,891,898,931,951,971,991", EFFECT_PROV, ",")
}
for (x in EFFECT_PROV) {
if ( $7 == "0") {
for (i in LINE) {
print $1 "_" $2 "_" $3 "_" $4 "_" EFFECT_PROV[x] "_" $6 "_" LINE[i]
}
} else {
print $1 "_" $2 "_" $3 "_" $4 "_" EFFECT_PROV[x] "_" $6 "_" $7
}
}
}