运行一直很稳定的质检系统突然出现了奇怪的现象,很早之前就上线的【暂存】功能出现了发布不了的bug。
先说排查结果:网络抖动等原因亦或是客户端操作系统连续点击或者并发访问造成同一时刻插入多条数据。
根据反应现象,质检员在做完【暂存】操作之后,同一条录音出现了两条数据,之后质检员做批量发布暂存数据操作,其中一条发布成功,一条无法发布。
之后我通过录音唯一id去数据库查询数据,一个名为【t_qa_record】的质检记录表中同一条录音的质检数据出现了两条,显然是不对的,故也对应了质检员反应的现象。
一开始觉得很奇怪,因为第一步查询数据库才正实了现象的存在。
接着第二步,去生产获取日志,观察日志中sql的执行记录。
通过检索日志发现在同一时刻(同一秒)相同的sql执行了两遍,初步猜测程序在执行的时候没有做任何去重判断,客户端可能因为网络原因连续点击了两次发布动作,然后又在同一时刻程序做出了响应,导致后台执行了两边sql的插入。再次证实了并发操作的可能性
第三步,跟踪程序代码,以下是找到的伪代码片段:
try {
...
/**
* 根据用户提交的数据,创建QARecord对象
*/
QARecord record = EntityFactory.create(context, QARecord.class);
//同一条录音记录只能对应一条质检记录
List<QARecord> records = service.findByRecId(record.getRecId());
if(records != null && records.size() > 0) {
...
service.update(old);
} else {
...
service.create(record);
}
return new NormalResponse(record);
} catch (Exception e) {
e.printStackTrace();
return new ExceptionResponse(e);
}
发现程序逻辑是做了重复判断的校验,即当发现质检记录表中出现了一条质检记录时候不再插入新的数据,而是做更新操作。
最终处理:首先是多插入数据的清理,保证系统数据的准确性。第二就是质检记录表中标识唯一录音的ID做唯一索引,在数据库层面做唯一限制,这样即使再次出现以上现象多余重复数据也只会插入失败。
数据库创建唯一索引
create unique index UNIQUE_RECID on T_QA_RECORD (REC_ID)
创建失败。这又是什么问题。好吧,继续排查处理,最终发现原来质检记录表中存在历史遗留重复数据,看来这个bug很早就发生了,只不过一直没有被发现。
第四步,处理历史数据,创建唯一索引
首先根据唯一ID值数据库查询数量大于1的质检记录
select t.rec_id from obrecordprod.T_QA_RECORD t group by t.rec_id having count(t.rec_id)>1
找到44条数据,也就是这44条质检记录都是有问题的
然后观察统一的规律,查看重复数据中哪些数据可以删除。最终发现最终更新时间【last_update_time】最大时间的数据是需要保留的,其余可以删除。现在需要写一个sql进行数据的筛选
1)随便拿其中一个重复质检记录数据的id做实例查询,首先是获取最大时间的并且按照唯一ID(rec_id )做分组查询
SELECT rec_id, MAX(tr.last_update_time) ut
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN ('2657955')
GROUP BY tr.rec_id
2)因为最大时间的数据有可能是一样的,无法保证做到唯一筛选,故再做一层筛选:
SELECT MAX(id) id
FROM (SELECT rec_id, MAX(tr.last_update_time) ut
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN ('2657955')
GROUP BY tr.rec_id) a,
(SELECT DISTINCT rec_id, tr.last_update_time ut, tr.id
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN ('2657955')) b
WHERE a.rec_id = b.rec_id
AND a.ut = b.ut
GROUP BY a.rec_id, a.ut
3)然后是查询出来的九条数据中需要排除掉查询出来的这条最大ID值,最大更新时间的数据,其余八条就是待删除的数据,最终整理得出以下sql:
select *
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN ('2657955')
AND tr.id NOT IN
(SELECT MAX(id) id
FROM (SELECT rec_id, MAX(tr.last_update_time) ut
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN ('2657955')
GROUP BY tr.rec_id) a,
(SELECT DISTINCT rec_id, tr.last_update_time ut, tr.id
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN ('2657955')) b
WHERE a.rec_id = b.rec_id
AND a.ut = b.ut
GROUP BY a.rec_id, a.ut)
经过上面的过滤,九条中保留了最后更新时间最大,ID值最大的数据,其余查询出来的八条需要做删除处理。
结合上面过滤出来的44个rec_id的数据,最终版sql:
with xxx as
(select t.rec_id
from obrecordprod.T_QA_RECORD t
group by t.rec_id
having count(t.rec_id) > 1)
delete
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN (select xxx.rec_id from xxx)
AND tr.id NOT IN
(SELECT MAX(id) id
FROM (SELECT rec_id, MAX(tr.last_update_time) ut
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN (select xxx.rec_id from xxx)
GROUP BY tr.rec_id) a,
(SELECT DISTINCT rec_id, tr.last_update_time ut, tr.id
FROM OBRECORDPROD.t_qa_record tr
WHERE tr.rec_id IN (select xxx.rec_id from xxx)) b
WHERE a.rec_id = b.rec_id
AND a.ut = b.ut
GROUP BY a.rec_id, a.ut)
至此重复数据过滤删除完毕,创建唯一索引成功,问题得到最终的解决。