文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

线上业务优化之案例实战

2024-11-30 04:09

关注

本文是我从业多年开发生涯中针对线上业务的处理经验总结而来,这些业务或多或少相信大家都遇到过,因此在这里分享给大家,大家也可以看看是不是遇到过类似场景。本文大纲如下,

项目大纲

后台上传文件

线上后台项目有一个消息推送的功能,运营新建一条通知消息时,需要一起上传一列包含用户 id 的文件,来给文件中包含的指定用户推送系统消息。

如上功能描述看着很简单,但是实际上处理上传文件这一步是由讲究的,假如说后台上传文件太大,导致内存溢出,又或者读取文件太慢等其实都是一些隐性的问题。

对于技术侧想要做好这个功能,保证大用户量(比如达到百万级别)下,上传文件、发送消息功能都正常,其实是需要仔细思考的,我这里给出我的优化思路,

上传文件类型选择

通常情况下大部分用户都会使用 Excel 文件作为后台上传文件类型,但是相比 Excel 文件,还有一种更加推荐的文件格式,那就是 CSV 文件。

CSV 是一种纯文本格式,数据以文本形式存储,每行数据以逗号分隔,没有任何格式化。

因此 CSV 适用于简单、易读、导入和导出的场景,而且由于 CSV 文件只包含纯文本,因此文件大小通常比 Excel 文件小得多。

但是 CSV 文件针对复杂电子表格操作的支持就没 Excel 功能那么强大了,不过在这个只有一列的文件上传业务里够用了。

假如说上传文件中包含 100 万用户 id,那么这里使用 CSV 文件上传就有明显优势,占用内存更少,处理上传文件也更快。

消息推送状态保存

由于大批量数据插入是一个耗时操作(可能几秒也可能几分钟),所以需要保存批量插入是否成功的状态,在后台中还需要显现出这条消息推送状态是成功还是失败,方便运营人员回溯消息推送状态。

批量写入

针对这里上传大文件时的批量写入场景,这里提几个点大家注意一下就行,

rewriteBatchedStatements=true

MySQL 的 JDBC 连接的 url 中要加 rewriteBatchedStatements 参数,并保证 5.1.13 以上版本的驱动,才能实现高性能的批量插入。

MySQL JDBC 驱动在默认情况下会无视 executeBatch()语句,把我们期望批量执行的一组 sql 语句拆散,一条一条地发给 MySQL 数据库,批量插入实际上是单条插入,直接造成较低的性能。只有把 rewriteBatchedStatements 参数置为 true, 驱动才会帮你批量执行 SQL。另外这个选项对 INSERT/UPDATE/DELETE 都有效。

是否启用事物功能

批量写入场景里要不要启用事物,其实很多人都有自己的看法,这里我给出启用于不启用的利弊,

在本文提到的大文件上传批量写入的场景下,要是追求极致性能我推荐是不启用事务的。

假如在批量写入过程中发生网络波动或者数据库宕机,我们其实只需要重新新建一条通知消息,然后重新上传包含用户 id 的文件即可。

因为上一条通知消息因为批量插入步骤没有全部完成,所以推送状态是失败。后续等开发人员处理一下脏数据即可。

大事务

@Transactional 是 Spring 框架提供得事务注解,相信这是许多人都知道的,但是在一些高性能场景下,是不建议使用的,推荐通过编程式事务来手动控制事务提交或者回滚,减少事务影响范围,因而提升性能。

使用事务注解

如下是一段订单超时未支付回滚业务数据得代码,采用 @Transactional 事务注解

@Transactional(rollbackFor = Exception.class)
public void doUnPaidTask(Long orderId) {
    // 1. 查询订单是否存在
    Order order = orderService.getById(orderId);
    ,,,

    // 2. 更新订单为已取消状态
    order.setOrderStatus((byte) OrderStatusEnum.ORDER_CLOSED_BY_EXPIRED.getOrderStatus());
    orderService.updateById(order);
    ...
    // 3. 订单商品数量增加
    LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery();
    queryWrapper.eq(OrderItem::getOrderId, orderId);
    List orderItems = orderItemService.list(queryWrapper);
    for (OrderItem orderItem : orderItems) {
        Long goodsId = orderItem.getGoodsId();
        Integer goodsCount = orderItem.getGoodsCount();
        if (!goodsDao.addStock(goodsId, goodsCount)) {
            throw new BusinessException("秒杀商品货品库存增加失败");
        }
    }

    // 4. 返还用户优惠券
    couponService.releaseCoupon(orderId);
    log.info("---------------订单orderId:{},未支付超时取消成功", orderId);
}

可以看到上面订单回滚的代码逻辑有四个步骤,如下,

  1. 查询订单是否存在
  2. 更新订单为已取消状态
  3. 订单商品数量增加
  4. 返还用户优惠券

这里面有个问题,订单回滚方法里面其实只有 2、3、4 步骤是需要在一个事物里执行的,第 1 步其实可以放在事物外面来执行,以此缩小事物范围。

使用编程式事务

使用编程式事务对其优化后,代码如下,

@Resource
private PlatformTransactionManager platformTransactionManager;
@Resource
private TransactionDefinition transactionDefinition;

public void doUnPaidTask(Long orderId) {
    // 启用编程式事务
    // 1. 在开启事务钱查询订单是否存在
    Order order = orderService.getById(orderId);
    ...
    // 2. 开启事务
    TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
    try {
        // 3. 设置订单为已取消状态
        order.setOrderStatus((byte) OrderStatusEnum.ORDER_CLOSED_BY_EXPIRED.getOrderStatus());
        orderService.updateById(order);
        ...
        // 4. 商品货品数量增加
        LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery();
        queryWrapper.eq(OrderItem::getOrderId, orderId);
        List orderItems = orderItemService.list(queryWrapper);
        for (OrderItem orderItem : orderItems) {
            Long goodsId = orderItem.getGoodsId();
            Integer goodsCount = orderItem.getGoodsCount();
            if (!goodsDao.addStock(goodsId, goodsCount)) {
                throw new BusinessException("秒杀商品货品库存增加失败");
            }
        }

        // 5. 返还优惠券
        couponService.releaseCoupon(orderId);
        // 6. 所有更新操作完成后,提交事务
        platformTransactionManager.commit(transaction);
        log.info("---------------订单orderId:{},未支付超时取消成功", orderId);
    } catch (Exception e) {
        log.info("---------------订单orderId:{},未支付超时取消失败", orderId, e);
        // 7. 发生异常,回滚事务
        platformTransactionManager.rollback(transaction);
    }
}

可以看到采用编程式事务后,我们将查询逻辑排除在事务之外,这样也就减小了事物影响范围。

在极高性能优先的场景下,我们甚至可以考虑不使用事务,使用本地消息表 + 消息队列来实现最终一致性就行 。

海量日志采集

公司线上有一个项目的客户端,采用 tcp 协议与后端的一个日志采集服务建立连接,用来上报客户端日志数据。

在业务高峰期下,会有同时成千上万个客户端建立连接,实时上报日志数据。

在上面的高峰期场景下,日志采集服务会有不小的压力,如果程序代码逻辑处理稍有不当,就会造成服务卡顿、CPU 占用过高、内存溢出等问题。

为了解决上面的大量连接实施上报数据的场景,日志采集服务决定使用 Netty 框架进行开发。

这里直接给出日志采集程序使用 Netty 后的一些优化点,

采集日志异步化

针对客户端连接上报日志的采集流程异步化处理有三个方案,给大家介绍一下,

采集日志压缩

对上报后的日志如果要再发送给其他服务,是需要进行压缩后再处理,这一步是为了避免消耗过多网络带宽。

在 Java 里通常是指序列化方式,Jdk 自带得序列化方式对比 Protobuf、fst、Hession 等在序列化速度和大小的表现上都没有优势,甚至可以用垃圾形容。

Java 常用的序列化框架有下面这些,

如果兼容性要求不高可以选择 JSON,如果要求效率以及传输数据量越小越好则 PB/Thrift/Avro/Hessian 更合适。

数据落库选型

像日志这种大数据量落库,都是新增且无修改得场景建议使用 Clickhouse 进行存储,好处是相同数据量下对比 MySQL 占用存储更少,查询速度更快,坏处就是并发查询性能比较低,相比 MySQL 使用不算那么成熟。

来源:今日头条内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯