这是新手期间第一个上线功能搞出的bug,同时也明白了一个道理:1. 线上环境总是复杂的,不可预知的,一定要做好各种准备; 2. 重要的功能要做放重复提交;3. 基础要打打牢。
需求
需求其实很简单,就是一个修改密码的入口,用户输入提交表单后,如果之前没有设置过密码,则设置,否则修改。然后验证密码的逻辑就不用说了,查出表里的密码,然后对比。
项目是ibatis+oralce,之前DAO的逻辑如下
...
if (isNotExists()){
insertPassword;
} else {
updatePassword;
}
而isNotExists()
方法就是select看是否存在记录。
public Map queryPassword(Map param) throws DAOException {
return sqlMapClientTemplate.queryForObject(param);
}
问题
某一天中午突然来了告警日志,发现在queryPassword
这个方法这里抛出了too many results
异常,很显然,本应该每个用户只存在一条记录的,查出了多条。让运维同事查出数据,果然有一个用户,存在3条记录。查看原代码逻辑,那很显然是在判断是否存在记录这里同时来了三个请求,然后三个线程都发现该用户对应的记录是空的,于是同时插入了三条记录。(后来通过电话回访用户,说当时手机页面卡了,于是瞎点一通,可能页面卡了,等反应后一下子发出去多条请求。)
解决和复盘
- 首先线上问题紧急的解决方案就是删掉记录就OK了。
然后这种
if ... then...
的用法是非常危险的,一来一回,在并发情况下就出现问题了。当时想着改个密码对同一个用户哪儿来的多并发,先就这样干吧,事实证明线上环境的复杂是永远超出自己预知的。对于这种场景,最好使用oralce提供的merge方法,把事务处理交给oracle,出错的概率更小一些。merge into pwd_table t using dual on (t.id = #id#) when matched then update pwd_table t set t.pwd = #pwd# where t.id = #id# when not matched then insert into pwd_table t values(#id#, #pwd#)
- 做防重复提交限制,考虑到用户的群体,最简单的是在用户提交请求得到响应之前,按钮置为diabled状态
基础一定要打好,当时就是对merge了解不深没有使用,要多学习PL/SQL的东西