1. 背景
redis 被用做内存数据库,一些需要基础数据从数据库筛选出来,拼接成字符串放入 redis 的 set
和 hash
结构中。
基础数据有生效、失效时间,现有是通过 quartz job 定时刷入、刷出。后来接到投诉,发现数据库和 redis 数据不一致现象,原因未知,因此需要做数据比对及数据同步。
2. 思路
2.1. 方案一
步骤:
- 筛选数据库数据到中间表A
- 根据入 redis 规则,将对应 key 中的数据导出并拆分入中间表B
- 编写SQL比对中间表A、B
优点: 统计结果应该比较直观,数据准备好,SQL比较2个表的差异还是很容易写出来
缺点: 数据库有几个字段涉及拆分,比如表示全国的值,要拆分很多个省份值,这个拆分工作以我的SQL水是搞不定的。
总结: 效率不好说,单就是一个数据库表就要建2个辅助中间表,这样的扩展性就不会太高,
2.2. 方案二
步骤:
- 筛选数据库数据导出到文件A
- 写脚本做数据拆分
- 根据入 redis 规则,将对应导出的文件转换成 pipe 数据文件
- 将数据文件导入 new_key
- 在事务内执行 redis 重命名:rename key old_key 及 rename new_key key
- 导出 redis 中 old_key 的数据到文件B
- 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. 方案三
步骤:
- 筛选数据库数据导出到文件A
- 写脚本做数据拆分
- 导出 redis 中的数据到文件B
- 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
| 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: sqlplus -s ${DB_LINK}<<EOF SET COLSEP ; SET ECHO ; SET FEEDBACK ; SET TERMOUT ; SET NEWPAGE ; SET HEADING ; SET SPACE ; SET PAGESIZE ; SET TRIMOUT ; SET TRIMSPOOL ; SET LINESIZE ; 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拆分文件
- 将省份是000的,拆分成所有省份;否则将省份字段直接按逗号分隔。
- 将产品线是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 } } }
|