一个用户关联了多个账户异常分析与解决方案
一、发现问题
线上发现请求 /acm/queryAc 接口报错,查看日志发现如下报错信息
二、分析问题
分析日志发现是sql报错,由于sql查询结果集存在多条数据返回,但是selectOne()方法只有一个对象来接收,导致抛出org.apache.ibatis.exceptions.TooManyResultsException 异常。
1、查看dao接口
@Mapper
public interface AcmAcDao extends BaseDao<AcmAcBO> {
AcmAcBO getByUserId(@Param("userId")String userId);
AcmAcBO getByUserIdForUpdate(@Param("userId") String userId);
}
2、查看sql语句
select
from acm_ac
where USER_ID = #{userId,jdbcType=VARCHAR}
3、查看数据库相关记录
该用户在数据库中存在两个账户记录,但是dao层接口只接收一条记录。
进一步分析出现一个用户关联多个账户的原因:
通过sql 查看表结构 show create table acm_ac;
CREATE TABLE acm_ac
(
AC_ID
varchar(64) NOT NULL COMMENT ‘账户ID’,
USER_ID
varchar(64) NOT NULL COMMENT ‘用户ID’,
ANDROID_BAL
decimal(15,2) NOT NULL DEFAULT ‘0.00’ COMMENT ‘安卓余额’,
APPLE_BAL
decimal(15,2) NOT NULL DEFAULT ‘0.00’ COMMENT ‘苹果余额’,
AC_STATUS
char(16) NOT NULL DEFAULT ‘NORMAL’ COMMENT ‘账户状态:正常 NORMAL 冻结 FORZEN’,
CREATE_TIME
datetime DEFAULT NULL COMMENT ‘创建时间’,
MODIFY_TIME
datetime DEFAULT NULL COMMENT ‘修改时间’,
UPDATE_DATE
datetime DEFAULT NULL COMMENT ‘修改时间’,
CREATE_DATE
datetime DEFAULT NULL COMMENT ‘创建时间’,
PRIMARY KEY (AC_ID
),
UNIQUE KEY USER_ID_2
(USER_ID
),
KEY AC_ID
(AC_ID
),
KEY USER_ID
(USER_ID
)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
结果显示USER_ID字段没有添加唯一约束,
进一步排查出现一个用户存在两条账户信息的原因:
首先检查账户添加的逻辑,在项目全局检查添加账户逻辑出现在账户充值逻辑和查询账户记录两个函数中,
虽然代码逻辑方面没有问题,但是并发场景下还是会出现一个用户关联多个账户的情况。
三、解决问题
由于账户涉及用户金额,紧急的解决方案是修改sql语句,可以先解决代码报错问题,修改如下
select
from acm_ac
where USER_ID = #{userId,jdbcType=VARCHAR} order by create_time desc limit 1
通过增加order by create_time desc limit 1 sql片段可以保证一个用户最多只返回一条记录
另外,需要把数据库重复数据清除掉,由于ANDROID_BAL和APPLE_BAL分别保存的是安卓账户余额和苹果账户余额,需要对多条记录的这2个字段做统计汇总。因此本方案使用mysql的聚合函数处理重复数据,并结合rename函数解决,编写脚本如下:
SET AUTOCOMMIT=0;
begin;
create table acm_ac_new like acm_ac;
rename table acm_ac to acm_ac_old;
insert into acm_ac_new
select t.AC_ID,t.USER_ID ,sum(t.ANDROID_BAL) ANDROID_BAL,sum(t.APPLE_BAL) APPLE_BAL,t.AC_STATUS,t.CREATE_TIME,t.MODIFY_TIME,t.UPDATE_DATE,t.CREATE_DATE from acm_ac_old t GROUP BY USER_ID;
alter table acm_ac_new add UNIQUE (user_id);
rename table acm_ac_new to acm_ac;
commit;
验证:
select count(*) num,t.user_id from acm_ac t GROUP BY t.user_id having num>1;
select count(DISTINCT(user_id)) from acm_ac;
select count(DISTINCT(user_id)) from acm_ac_old;
注意事项:
一、添加唯一约束需要在 rename table acm_ac_new to acm_ac; 之前执行,在脚本执行期间由于rename table acm_ac to acm_ac_old;语句把acm_ac表名改为acm_ac_old,在此期间线上应用程序如果查询这个表会出现表找不到的sql报错,所以需要在运维期间执行此脚本;
二、执行这个脚本之前需要先评估下耗时情况,本例在线上执行之前先在预发数据库执行,多次测试耗时在4秒左右,会造成线上服务器账户模块短暂的异常,需要做好相关处置措施。