巴别塔合约作战终端开发日记4——后端效率优化

注册优化

注册功能存在很严峻的短时间访问压力,并且这个功能会直接影响用户体验(我连账号都注册不了何谈后续呢?)。为了防止这个问题的发生,一共经历了三个阶段的修改:

第一版:写锁+数据库判重+直接数据库插入

虽然是初版代码,以实现功能为目的。但也只能说,这种操作写出来就是注定两个字:失败。 MySQL作为效率瓶颈,虽然代码逻辑正确,先加锁,再判重,再入库。但是强行将多线程变为单线程,然后再使用效率瓶颈判重,虽然最后一步逃不掉,但是每次数据库操作都需要连接,虽然有连接池的存在。然后再加上通信开销,这效率想想就爽啊。实测QPS大概在30左右。就这效率也想给用户体验?这不是折磨用户么?这不改天理难容。

第二版:分布式锁+Redis判重+直接数据库插入

根据账号的长度和首字母进行分类,每个种类的账号判重是互不影响的(首字母不同,长度也不同还能重?)。将单线程判重变为多线程判重。判重位置从数据库改成Redis,解决了MySQL慢的问题。最后插入数据库怎么解决,慢就慢呗,数据总不能不持久化了吧。

实测QPS在80左右,这个效率还是很不错的,但是转折点是在分布式锁这个位置,毕竟将单线程变为了多线程。然后就是又一个问题了,为啥还是这么慢,100都没过。后来想到了可能是Redis的问题,检查了一下Redis的执行时间,发现确实,Redis分布式锁在请求的时候大量请求压在Redis上,而且Redis使用的是单线程处理操作,这个效率就不见得很高了。然后就是最致命的,插入数据库还是浪费了很多时间。这效率也不过关,还能操作。

第三版:分段锁+Redis配合内存判重+直接数据库插入

思路与第二版一致,唯一的区别就是把一部分操作放到了内存上:将锁从分布式锁改成了Synchronized,然后在堆里开了一块儿内存来存放账号,内存判重配合Redis判重做操作。

这个思路牺牲了内存来换取效率,开56把锁内存是撑得住的,但是大量账号堆积在内存里怎么办?这个好办,为啥要用Redis配合,开一个定时任务每10秒执行一次,检查账号池里是否有数值,有的话就全存到缓存中去。这个位置只要能赌在内存中发现重复,就能有效减少一次Redis的连接开销。这样的话内存只要不在10秒内爆炸,就可以正确运行。但是实测下来QPS也就100左右没高多少,唯一还有问题的就是最后一步插入数据库了,打印了一下执行时间,发现确实,时间都浪费在最后一步了,但是这一步始终逃不掉,怎么优化呢?

第四版:分段锁+Redis配合内存判重+数据库批处理

思路与第三版一致,但是优化数据库操作:将判重结束的用户信息留在内存,通过自生成ID直接返回,设立一个定时任务,定时向数据库执行一个批处理插入,这样就可以极大幅度降低最后一步入库的时间,这样绝大部分操作都被放到了内存上,执行效率那能不快吗?

这个思路其实还可以优化,就是将分段锁的synchronized换成CAS,减少锁开销,但是在高并发量的情况下可能很消耗CPU,不过实际用户数量也不会特别多,那CAS其实是最优解。由于偏向锁的出现,在低并发情况下,锁开销实际也还好,然后再加上项目临近上线,也没太多时间测试,就没有继续优化了。

最后其实还有一个很关键的东西就是:每次执行插入操作也就是insert()方法时,由于使用的是远程连接,那么理论上都会产生一个与数据库通信的开销。实测了一下确实是这样的。然后MyBatisPlus自封装有insertBatch()批量插入,但是翻看了源码,底层使用的是迭代器循环调用insert()方法。那这样批处理就没有了意义啊,还是要吃通信开销的时间损耗。没有办法,使用HttpClient并额外编写了一个模块部署在数据库服务器上,每次批处理不调用MyBatisPlus的批量插入,直接使用HTTP请求发送数据到数据库服务器上,让模块执行本地批量存储,这样就相当于使用了一次通信损耗,插入了若干条数据。实测下来,最后这样做总耗时20-30毫秒,但是执行常规批处理是没有这种效率的。

第四版就非常舒服了,QPS实测在300左右,这优化还是杠杠的。

2022/3/1 补充:今天在维护项目的时候,发现对注册功能进行了限流。起初的意愿是:害怕并发过高打垮数据库,使用了令牌桶算法进行了限流。但是经过这么多轮优化,注册功能没有直接操作过数据库。那我这限流限了个啥啊????把限流的AOP实现去掉之后,本机注册一秒直接跑完了3000条请求。放服务器上肯定也会更快。(亏了。。。我投出去的简历QPS都写得很低,都亏了。。。。。)

分段锁思想

无论在哪个操作里,对A的操作永远与B无关,如果强行使用锁来保证线程安全,那执行效率是一个很大程度的损耗。于是,我们为什么不能把本身就相互无关的东西放行呢?

详细解释一下就是:所有与用户A相关的操作,那么在用户B执行操作的时候,两者只要没有使用共享资源,那么这些操作完全无关,不会产生并发问题。如果此时只用一个synchronized来操作,那就相当于白白浪费了执行时间。

回归到实现上,synchronized锁需要对象,lock同样也需要自己创建对象使用。那么假设现在有10000个用户,上哪去找10000个锁来呢?可能会想到,锁字符串吧,这个好搞。但是字符串由于存在常量池的问题,使用锁是不允许锁字符串的,容易发生意想不到的问题。

那么,分布式锁就可以很方便地使用,使用Redis分布式锁只需要使用key标记就可以,通过id来控制key生成就可以。本项目使用的分布式锁Redisson ,直接导入maven依赖,然后添加配置即可。

<!-- 分布式锁Redisson -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.6.5</version>
        </dependency>
@Configuration
public class RedisConfiguration {

    @Value(value = "${RedissonPassword}")
    private String password;
    @Value(value = "${RedissonHost}")
    private String redissonAddress;

    //向Bean容器中添加Redisson分布式锁
    @Bean
    public Redisson redisson(){
        Config config = new Config();
        //此时为单机模式
        //绑定Redis所在的IP和使用哪个DataBase,这里设置为第0个Database
        SingleServerConfig singleServerConfig = config.useSingleServer().setAddress(redissonAddress).setPassword(password).setDatabase(0);

        return (Redisson)Redisson.create(config);
    }

}

添加至容器之后 ,使用时只需要@Autowired注入,然后像Lock类一样使用即可。

@Autowired
Redisson redisson;

String lockKey = "Challenger::"

#获得锁,锁的是目标key
RLock lock = this.redisson.getLock(lockKey);

#加锁
lock.lock();
#解锁 记得写在final里
lock.unlock();

数据库处理逻辑优化

数据库处理逻辑方面,由于使用了MySQL远程连接,远程连接通信也是需要消耗资源的。在获取连接的开销上额外增加消耗,也就是说,每一次执行sql语句,代价都是非常大的,对于部分并发要求较高的程序,这会成为严重的效率瓶颈。

对此,本项目中的注册功能,在活动开启当天肯定是热点功能,为了不影响效率,就需要额外优化。

注册业务的优化在注册优化部分提到过,其余的业务做到的是:能批处理就绝对不单独去执行一条sql语句。使用远程连接给我最大的一个感觉就是,即便使用了@Transcational注解使用事务处理,同一个连接处理两条sql语句的效率也是很感人的。这是真实的感受,实际打印一下执行效率就能看到了,真的效率感人。

Redis性能优化

Redis作为高速缓存,在存储热点key提高响应速度这是公认的,但是,在项目实际使用的时候,Redis往往响应速度并不是很理想,在查询审核记录方面,在1000并发的情况下,会出现Redis执行速度逐渐变慢的问题。竟然拖慢了我的访问效率?这可是读请求啊,这再反应慢,项目也太失败了吧。

看到这个问题的时候,第一反应是回归到硬件上,硬件太差了导致Redis反应不理想。然后发现其实并不是这样的。转念想一下,去打印了一个简单读取数字的Redis请求,发现秒并发量10000的情况下,返回一直都很快,没有出现拖沓的情况。这个时候就很不解了,这是啥啊?然后又对比了一下Set的存取和直接key的存取,发现效率其实没差。

最后终于定位到问题原因了,由于审核列表在后期会越来越多,测试用的账号有10+条审核记录,存储的都是Java类型,一个对象的数据有不少。这样就会出现一个情况:Redis反应是很快,但是时间都浪费在打包给你返回数据上了,要返回的数据太大了。

最后对大型数据做了一个分页,这个问题就完美解决了,每次只返回一部分数据,减少数据量。一次性返回那么多数据,用户实际想要查看的并不是很多。

异步处理

对于耗时业务,比如邮件发送(这个是真的很耗时),部分操作涉及的日志存储和不重要的数据处理(与代币消费有关的消费记录日志)。都可以使用多线程来做,提高响应速度。

在异步处理方面使用的策略是:线程池。

线程的创建和销毁也是需要消耗资源的,那既然这些操作都是频繁出现的,那就直接搞一两个线程帮我们后台处理就好了,需要直接拿来用,不要了就直接还就好了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值