【异常】记一次因错误运用数据冗余,导致的数据不一致的生产事故
一、需求描述
公司今年发米和油,因为太重了,所以新年活动礼包都采取了线上领取的方式来开展,目前功能的设计是,如果还没有下单,系统支持用户对领取的地址进行修改。测试同学都测试了好多轮了,没有问题,而且这之前功能模块也是在生产环境,做过活动的。
二、问题描述
用户在H5前段礼品领取过程看起来岁月静好啊,没有什么问题,也没有很多用户说领取不了的问题。但是,到了后段物流发货的功能,就不行了。在一个岁月静好的周末,紧急收到了运营的电话反馈,很多用户虽然在H5端正常的修改与展示了地址,但是系统依旧没有进行正常保存,导致了合作方在发货时,存在了收货地址与实际不符的问题,给他们的工作造成了很大的困扰、
聚焦问题就是在用户界面虽然地址已经更新了,但是导出的领取记录中,依旧是旧的地址不更新,即导出的地址信息与实际用户修改的地址不符的情况,这会导致了供应商收到的地址是错的,发错了导致的了很多物流的问题。
以下是具体的排查过程。
三、重现用户操作
3.1 重现操作1
用户在主题活动的页面,主动点击修改按钮修改了地址→点击编辑→编辑地址并保存→回到地址列表,直接点击了左上角的返回→回到活动页,看到修改后的地址。
3.2 重现操作2
公司的新年活动礼包的领取活动中,用户在【我的】→【收货地址】页面修改了地址,再回到活动页面看到地址已经修改了,就以为已经修改成功了,实际上此时活动地址没有真正被修改成功。
四、问题排查过程
4.1 SQL查询异常收货地址
通过SQL的查询,我们后台识别了总共有90+的用户是存在着错误操作
以下是具体的排查SQL语句脚本,也是修复异常的脚本
SELECT * FROM
(
SELECT *,
( SELECT user_name FROM user_address WHERE id = receive_address_id ) AS new_user_name,
( SELECT tel_num FROM user_address WHERE id = receive_address_id ) AS new_tel_num,
( SELECT CONCAT( province_name, city_name, county_name ) FROM user_address WHERE id = receive_address_id ) AS new_third_path,
( SELECT detail_info FROM user_address WHERE id = receive_address_id ) AS new_full_path
FROM theme_activity_user USER WHERE activity_id = '1610210477824540673'
) AS t
WHERE receive_address != new_full_path
4.2 相关的表设计
4.2.1 用户收货地址表:user_address
CREATE TABLE `user_address` (
`id` varchar(32) NOT NULL COMMENT 'PK',
`tenant_id` varchar(32) NOT NULL COMMENT '所属租户',
`del_flag` char(2) NOT NULL DEFAULT '0' COMMENT '逻辑删除标记(0:显示;1:隐藏)',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
`user_id` varchar(32) NOT NULL COMMENT '用户编号',
`user_name` varchar(50) DEFAULT NULL COMMENT '收货人名字',
`postal_code` varchar(50) DEFAULT NULL COMMENT '邮编',
`province_name` varchar(50) DEFAULT NULL COMMENT '省名',
`city_name` varchar(50) DEFAULT NULL COMMENT '市名',
`county_name` varchar(50) DEFAULT NULL COMMENT '区名',
`detail_info` varchar(50) DEFAULT NULL COMMENT '详情地址',
`tel_num` varchar(50) DEFAULT NULL COMMENT '电话号码',
`is_default` char(2) DEFAULT NULL COMMENT '是否默认 1是0否',
PRIMARY KEY (`id`) USING BTREE,
KEY `ids_tenant_id` (`tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='用户地址';
4.2.2 用户主题活动领取记录表:theme_activity_user
CREATE TABLE `theme_activity_user` (
`id` varchar(30) NOT NULL DEFAULT '' COMMENT 'ID',
`activity_id` varchar(30) NOT NULL DEFAULT '' COMMENT '活动ID',
`user_id` varchar(30) DEFAULT NULL COMMENT '用户ID',
`work_no` varchar(30) NOT NULL DEFAULT '' COMMENT '工号',
`phone` varchar(20) NOT NULL DEFAULT '' COMMENT '手机号',
`user_name` varchar(30) DEFAULT NULL COMMENT '用户姓名',
`company` varchar(40) DEFAULT NULL COMMENT '公司',
`department` varchar(30) DEFAULT NULL COMMENT '部门',
`gift_ids` varchar(500) DEFAULT NULL COMMENT '礼品ID拼接字符',
`receive_address_id` varchar(30) NOT NULL DEFAULT '' COMMENT '地址ID',
`receive_name` varchar(30) DEFAULT NULL COMMENT '收货人名称',
`receive_phone` varchar(20) DEFAULT NULL COMMENT '收货人手机号',
`receive_region` varchar(255) DEFAULT NULL COMMENT '收货地区',
`receive_address` varchar(200) DEFAULT NULL COMMENT '收货地址',
`logistics_no` varchar(500) DEFAULT NULL,
`logistics_company` varchar(300) DEFAULT NULL,
`logistics_phone` varchar(300) DEFAULT NULL,
`logistics_carrier` varchar(300) DEFAULT NULL,
`user_status` char(1) NOT NULL DEFAULT '0' COMMENT '领取状态:0-待领取 1-已领取 2-已发货',
`receive_time` datetime DEFAULT NULL COMMENT '领取时间',
`delivery_time` datetime DEFAULT NULL COMMENT '发货时间',
`receive_num` int(11) DEFAULT '1' COMMENT '领取礼品数',
`receive_gift_ids` varchar(500) DEFAULT NULL COMMENT '领取产品ID',
`receive_gift_names` varchar(2000) DEFAULT NULL COMMENT '领取产品名称',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新者',
`remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='活动用户群体';
4.2.3 进一步说明
以下这些表的内容是冗余的内容,
receive_address_id
varchar(30) NOT NULL DEFAULT ‘’ COMMENT ‘地址ID’,
receive_name
varchar(30) DEFAULT NULL COMMENT ‘收货人名称’,
receive_phone
varchar(20) DEFAULT NULL COMMENT ‘收货人手机号’,
receive_region
varchar(255) DEFAULT NULL COMMENT ‘收货地区’,
receive_address
varchar(200) DEFAULT NULL COMMENT ‘收货地址’,
这些字段的内容,其实是跟用户收货地址表中的内容重复的
id
varchar(32) NOT NULL COMMENT ‘PK’,
user_name
varchar(50) DEFAULT NULL COMMENT ‘收货人名字’,
province_name
varchar(50) DEFAULT NULL COMMENT ‘省名’,
city_name
varchar(50) DEFAULT NULL COMMENT ‘市名’,
county_name
varchar(50) DEFAULT NULL COMMENT ‘区名’,
detail_info
varchar(50) DEFAULT NULL COMMENT ‘详情地址’,
tel_num
varchar(50) DEFAULT NULL COMMENT ‘电话号码’,
也就是我们常说的冗余。
我们都知道数据库的冗余能 提高查询性能,但是如何正确的运用冗余确实不容易。
4.2.4 查询地址接口源码
//初始化用户地址信息 与 收货信息
if (ObjectUtil.isNull(themeActivityUser.getReceiveAddressId())) {
return themeActivity;
}
initReceiveInfo(userId, activityId, themeActivity, themeActivityUser);
private void initReceiveInfo(String userId, String activityId, ThemeActivity themeActivity, ThemeActivityUser themeActivityUser) {
log.info("初始化用户地址信息 与 收货信息");
UserAddress userAddress = userAddressMapper.selectById(themeActivityUser.getReceiveAddressId());
ThemeActivityReceiveInfoDTO receiveInfo = new ThemeActivityReceiveInfoDTO();
//兼容用户删除了用户地址信息
if (ObjectUtil.isNull(userAddress)) {
log.info("用户地址已经删除,查询不到对应地址信息,receiveAddressId =【{}】", themeActivityUser.getReceiveAddressId());
receiveInfo.setReceiveName(ObjectUtil.isNotNull(themeActivityUser.getReceiveName()) ? themeActivityUser.getReceiveName() : "");
receiveInfo.setReceivePhone(ObjectUtil.isNotNull(themeActivityUser.getReceivePhone()) ? themeActivityUser.getReceivePhone() : "");
receiveInfo.setReceiveRegion(ObjectUtil.isNotNull(themeActivityUser.getReceiveRegion()) ? themeActivityUser.getReceiveRegion() : "");
receiveInfo.setReceiveAddress(ObjectUtil.isNotNull(themeActivityUser.getReceiveAddress()) ? themeActivityUser.getReceiveAddress() : "");
} else {
receiveInfo.setReceiveName(userAddress.getUserName());
receiveInfo.setReceivePhone(userAddress.getTelNum());
String provinceName = ObjectUtil.isNotNull(userAddress.getProvinceName()) ? userAddress.getProvinceName() : "";
String countyName = ObjectUtil.isNotNull(userAddress.getCountyName()) ? userAddress.getCountyName() : "";
String cityName = ObjectUtil.isNotNull(userAddress.getCityName()) ? userAddress.getCityName() : "";
receiveInfo.setReceiveRegion(provinceName + countyName + cityName);
receiveInfo.setReceiveAddress(ObjectUtil.isNotNull(userAddress.getDetailInfo()) ? userAddress.getDetailInfo() : "");
}
receiveInfo.setActivityId(activityId);
receiveInfo.setUserId(userId);
receiveInfo.setReceiveAddressId(themeActivityUser.getReceiveAddressId());
receiveInfo.setReceiveGiftIds(ObjectUtil.isNotNull(themeActivityUser.getReceiveGiftIds()) ? themeActivityUser.getReceiveGiftIds() : "");
receiveInfo.setReceiveGiftNames(ObjectUtil.isNotNull(themeActivityUser.getReceiveGiftNames()) ? themeActivityUser.getReceiveGiftNames() : "");
log.info("receiveInfo = 【{}】", JSONUtil.toJsonStr(receiveInfo));
themeActivity.setReceiveInfo(receiveInfo);
}
什么???竟然没次都去数据库做一轮查询?
那做数据库冗余的意义是什么?干脆直接废弃掉算了?
另外,哪里去做了该冗余数据的更新?好像并没有哦?
五、数据库的冗余与三范式
5.1 数据冗余是什么?
- 发生在数据库系统中
- 指的是一个字段在多个表里重复出现。
- 在一个数据库中存在多余的数据。
- 指数据之间的重复
- 同一数据存储在不同数据文件中的现象。
- 数据库数据中有重复信息的存在
- 指同一数据被反复存放,某一属性值发生改变其他与之相同的属性值也要改变。
增加数据的独立性和减少数据冗余是企业范围信息资源管理和大规模信息系统获得成功的前提条件。
正确使用数据冗余可以提高数据库的性能和可用性,正确使用冗余数据需要根据数据库的具体需求进行灵活的调整,需要认真分析数据库的查询和事务的需求,并结合数据库的具体性能进行调整。
5.2 数据冗余的缺点
5.2.1造成资源浪费
数据库存储的空间是一定的,如果冗余数据过多的话,会造成资源的浪费。
需占用较多的存贮空间,浪费存储空间,尤其是存储海量数据时。
5.2.2 维护成本提高,产生数据不一致的问题
增加了更新代价,代价高
其潜在的数据不一致问题,是产生数据不一致的根源
如果其中一个相关的字段发生变化,则另一个字段也必须相应地做出变化,否则就会出现信息矛盾或者不一致的现象。这对于保持数据一致性来说,是需要消耗维护成本的;
- 导致数据异常和损坏,一般来说设计上应该被避免。
5.3数据冗余举例
举个例子,如果每条客户购买商品的信息里都连带记录了客户自身的信息,这样的数据冗余可能造成不一致,因为客户自身的信息可能不一样。
5.4 数据冗余的处理方法
处理方法 | 具体说明 |
---|---|
服务同步双写 | 业务方调用服务,服务先插入T1,再插入T2返回插入成功。 优点:逻辑简单,一致性高。 |
服务异步双写 | 业务方调用服务,服务先插入T1,服务异步发出一个消息MQ,kafla调用另一个专门的服务来写入冗余数据。 |
线下异步双写 | 业务方调用服务,服务先插入T1,返回插入成功,数据会被写入到log表中,线下服务读取log表进行更新。 |
5.4 减少冗余数据的常见做法包括:
- 在数据库设计时尽量减少冗余数据,使用正确的数据类型和约束。
- 使用视图和存储过程来提高查询效率,在视图和存储过程中使用冗余数据。
- 使用数据库索引来提高查询效率,尽量使用联合索引。
- 使用数据复制和数据镜像来提高数据库的可用性。
- 使用数据库缓存来提高性能。
六、问题解决
修改地址的时候,传输用户最新的地址信息进行保存,而非地址ID
将原来直接关联查询地址表的返回地址数据的逻辑,修改为直接取活动领取记录中的冗余地址记录
七、总结
因错误运用数据冗余,导致的数据不一致的生产事故,RIP!