一次网络波动引发的事故 后端警醒
前言
一次网络波动 引发啦对事务的深层思考 我们对于Sql 优化已经很明白的 但是真正去使用的时候你又用了多少 我发现我在执行一个方法的时候 事务一直失败 也就是事务超时 如下报错:
查看事务的等待时间:
SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout';
也就是事务执行时间超过了最长等待时间 造成事务超时
查看执行的事务:
select * from information_schema.INNODB_TRX;
因为事务超时 这个就会产生资源占用后续操作长时间等待 直到资源释放 随着数据量的增大 产生的概率也会增加 是比较严重的生产事故 一旦出现后果大家都清楚 所以至少要明白 到出现这个问题 我们怎么去排查 而不是扎耳挠腮
查询性能优化
使用 Explain 进行分析
Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句,一个索引的使用情况是一个SQL语句执行的关键 如果大数据量下不使用索引 那事务超时的的概率就会大大增加。
比较重要的字段有:
- select_type : 查询类型,有简单查询、联合查询、子查询等
- key : 使用的索引
- rows : 扫描的行数
优化数据访问
1. 减少请求的数据量
- 只返回必要的列:最好不要使用 SELECT * 语句。
- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。
2. 减少服务器端扫描的行数
最有效的方式是使用索引来覆盖查询。
重构查询方式
1. 切分大查询
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
rows_affected = 0
do {
rows_affected = do_query(
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0
2. 分解大连接查询
将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:
- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
- 减少锁竞争;
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id=tag.id
JOIN post ON tag_post.post_id=post.id
WHERE tag.tag='mysql';
SELECT * FROM tag WHERE tag='mysql';
SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
大事务拆分
这个是非常有必要的 因为MySQl 数据库的事务等待时长是五十秒 超时就会造成资源等待的问题 这种情况主要是因为你的事务执行链条过长 或者SQL编写逻辑问题
大事务拆分思想
大事务指的是我们有一个业务,它设计了很多操作,所有的操作要么全部成功,要么全部失败,不能一部分操作成功,一个部分操作失败,当这些操作涉及过多的数据库表,如果有一个sql超时,或者执行缓慢,会把事务里面涉及到的表都锁住,性能严重受到影响。
举个例子,最常见的例子是银行转账:
-
需求
张三向李四转账200,李四的余额增加200,张三的余额减少200
-
操作步骤
1. 检查张三账户,进行余额扣减
2.银行调用转账接口
3.检查李四账户,余额增加
这个转账的动作,一定是要1,2,3步骤同时成功,才能算转账这个动作成功,只要有一个步骤失败,转账操作就是失败,如果这三个步骤,设计到很多数据库表的操作,如果有一个sql超时,没有及时释放表锁,就会导致其他客户端的锁等待超时,整个系统都会受到影响。
事实上,事务的目的是为了保证数据的原子性,准确性,那么也就是说,只要你需要保证的数据做到了,就可以进行事务提交了。所以,可以将大事务拆小,即保证最小事务的执行即可。如:更新一个用户的会员状态,那么只需要查出相关信息,更改状态,写入相应记录,该事务即可提交。
- 将大事务拆小后,就可以做到快速释放锁的作用,从而避免了其他客户端的锁等待超时问题了。
- 把查询相关的语句,放到事务外处理
拆分事务的目的是为了快速释放锁,避免死锁,提高程序的吞吐率
拆分例子:
我们之前做事务管理都是添加@Transactional
,但是这个注解只能加在类和方法上,对于方法比较大,但是只有其中部分代码我们需要做事务管理时可以使用。transactionManager。
@Autowired
private TransactionTemplate transactionTemplate;
@Override
public ChannelAddResponse add(ChannelAddRequest channelAddRequest) {
// 进行事务管理
transactionTemplate.execute(action ->{
User user = new User();
CopyParamNotNullUtil.copyPropertiesIgnoreNull(channelAddRequest, user);
user.setStatus(TaskStatusEnum.UNEXECUTED.getStatus());
userdao.save(user);
Dept dept = new Dept();
dept.setUser(user);
CopyParamNotNullUtil.copyPropertiesIgnoreNull(channelAddRequest, dept);
deptdao.save(dept);
return null;
});
}
转载自:https://juejin.cn/post/7153457854278533128