这是一个简单的数据生产导入的故事,原本故事情节应该是这样的:数据整理-->测试验证-->生产发布-->生产验证,然后就是各回各家,所以这本来应该是一个平淡的故事,然而实际却变成了如下情节:数据整理-->测试验证-->生产发布-->生产验证-->校验失败(预期数据未导入)-->问题排查-->解决问题-->生产发布-->生产验证-->校验问题(大部分数据是正确的,少部分数据不正确)-->问题排查(当时未能排查出原因,但能判断出异常与生产原有的几条异常数据有关)-->异常数据删除sql编写-->测试校验-->生产发布-->生产校验-->重新导入删除部分数据(异常数据这次直接排除,没包括在导入范围)-->部分异常数据请示领导修正-->修正Sql准备-->测试校验-->生产发布-->修正数据对应数据导入-->生产校验!
你以为到这里就结束了?NO NO NO,故事怎么可能就这么结束,因为这批数据导入有对应的其它业务,还需要执行该部分业务,最终确认后才能各回各家,结果发现,坑爹的数据库数据是修正了,但因为程序采用了Redis,异常数据还在Redis中,所以还要在Redis中删除该部分异常数据,还好程序部分对此有处理,直接删除没导致程序功能异常,至此本次发布才算结束,但此时也已经是凌晨0点了,这真是一个悲剧的故事……
首先需要介绍下本次导入的猪脚,一个预先写好,且已经发布至生产的存储过程,另外该猪脚所在场景是MySql,其大致代码精简后如下
1 DROP PROCEDURE IF EXISTS `usp_SadEvent`; 2 DELIMITER $$ 3 CREATE PROCEDURE `usp_SadEvent` 4 ( 5 IN identityNo VARCHAR(20), 6 IN uName VARCHAR(15), 7 IN cAmount LONG 8 ) 9 label_at_start: 10 BEGIN 11 12 SELECT @uid := id FROM `user` 13 WHERE identity_no=identityNo AND NAME=uName; 14 15 IF @uid IS NULL THEN 16 select identityNo,uName,0 ret; 17 LEAVE label_at_start; 18 END IF; 19 update account set balance=balance+cAmount where uid=@uid; 20 select identityNo,uName,1 ret; 21 END label_at_start$$ 22 DELIMITER ;
首先就是what the fuck的执行失败问题,调用该存储过程结果居然都是返回ret=0失败!!
还好当初写存储过程时,考虑到了结果查询,比较容易就发现为什么返回的uName是乱码?碰上这种问题,直觉就是数据库编码有问题,一查果然如此
问题查出来了,怎么修正呢,正常剧情当然是改数据库默认编码为utf8了,但改编码后Mysql必须要重启,生产环境是你想重启就能重启的吗?好吧,还好mysql存储过程支持指定编码,修改存储过程,在uName部分指定编码集是utf8(补充虽然数据库默认字符集是latin1,但实际里面的表在创建时都默认指定utf8了,所以导致一直没发现生产环境居然有默认编码集问题)
IN uName VARCHAR(15) character set utf8,
改完后执行批量调用存储过程的sql,大致如下
call usp_SadEvent(‘123131231313123132‘,‘张三‘,3000);#数据库有 call usp_SadEvent(‘123454566778899999‘,‘李四‘,5000);#数据库无李四,或数据库里叫李斯
这里特别标明第一条数据库里是有对应数据,第二条在数据库中是查不到用户数据的,执行结果居然发现第二条“李四”的金额,被加到了“张三”身上,这又是什么鬼!
其实问题就出在mysql的默认声明参数上,只要在上面的调用语句下面再加一句
call usp_SadEvent(‘123131231313123132‘,‘张三‘,3000);#数据库有 call usp_SadEvent(‘123454566778899999‘,‘李四‘,5000);#数据库无 select @uid;
执行结果很明显的就告诉你@uid是有值的,而且值为张三的uid,好吧,没想到Mysql中
SELECT @uid := id
这种隐式声明参数的方式居然会在整个对话期间内都有效,所以还是老老实实改成如下显式声明才能测试正确
declare u_id long; select `id` into u_id FROM `user`
最终完整修改后的存储过程应该如下
DROP PROCEDURE IF EXISTS `usp_SadEvent`; DELIMITER $$ CREATE PROCEDURE `usp_SadEvent` ( IN identityNo VARCHAR(20), IN uName VARCHAR(15) character set utf8, IN cAmount LONG ) label_at_start: BEGIN declare u_id long; select `id` into u_id FROM `user` WHERE identity_no=identityNo AND NAME=uName; IF u_id IS NULL THEN select identityNo,uName,0 ret; LEAVE label_at_start; END IF; update account set balance=balance+cAmount where uid=u_id; select identityNo,uName,1 ret; END label_at_start$$ DELIMITER ;
哎,事后描述问题似乎很简单,实际排查这些问题还是挺坑的,哎……