一次网络波动引发的事故 后端警醒

11,954 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第12天,点击查看活动详情

前言

一次网络波动 引发啦对事务的深层思考 我们对于Sql 优化已经很明白的 但是真正去使用的时候你又用了多少 我发现我在执行一个方法的时候 事务一直失败 也就是事务超时 如下报错:

图片.png

查看事务的等待时间:

SHOW GLOBAL VARIABLES LIKE 'innodb_lock_wait_timeout'; 

图片.png 也就是事务执行时间超过了最长等待时间 造成事务超时

查看执行的事务:

select * from  information_schema.INNODB_TRX;

图片.png

因为事务超时 这个就会产生资源占用后续操作长时间等待 直到资源释放 随着数据量的增大 产生的概率也会增加 是比较严重的生产事故 一旦出现后果大家都清楚 所以至少要明白 到出现这个问题 我们怎么去排查 而不是扎耳挠腮

查询性能优化

使用 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超时,没有及时释放表锁,就会导致其他客户端的锁等待超时,整个系统都会受到影响。

 事实上,事务的目的是为了保证数据的原子性,准确性,那么也就是说,只要你需要保证的数据做到了,就可以进行事务提交了。所以,可以将大事务拆小,即保证最小事务的执行即可。如:更新一个用户的会员状态,那么只需要查出相关信息,更改状态,写入相应记录,该事务即可提交。

  1. 将大事务拆小后,就可以做到快速释放锁的作用,从而避免了其他客户端的锁等待超时问题了。
  2. 把查询相关的语句,放到事务外处理

拆分事务的目的是为了快速释放锁,避免死锁,提高程序的吞吐率

拆分例子:

我们之前做事务管理都是添加@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;
        });
 
}