场景题-1
订单到期关闭
1、DelayQueue
无界阻塞队列,用于放置实现了Delayed接口的对象,基于PriorityQueue实现,可用于实现在指定的延迟时间之后处理元素。订单创建后放入队列中,然后使用一个常驻任务不停地执行扫描取出超时订单,理论上可用,但是实际因为DelayQueue是基于JVM内存的,所以订单量大的时候容易OOM,而且数据无法持久化,在机器重启后数据会丢失。虽然配合数据库或者rediis等可以解决持久化问题,但是目前应用基本是集群部署,多个实例上的DelayQueue如何配合使用也是个问题,所以综合来说并不推荐(单机、订单量小可尝试)。
2、时间轮
时间轮可以理解为一种像钟表一样的环形结构,圆环上被划分出许多槽位(粒度可以是毫秒、秒、分钟、小时等),每个槽位上有一个链表用于保存所有到期的任务,随着时间推移指针指向哪个槽位就执行该槽位上的任务,另外引入round(圈数)和分层时间轮的概念来满足不同的任务时间精度。
2.1 使用Netty的HashedWheelTimer我们可以实现一个时间轮,但是和Delay有一样的问题。
2.2 使用Kafka的时间轮,Kafka有很多延时性的操作,延时生产、延时拉取和延时数据删除等,具体实现是Kafka下的TimingWheel类。Kafka的时间轮性能不错,但是实现方式较复杂。
3、定时任务
基于Timer、ScheduledThreadPoolExecutop或者xxx-job这类调度框架实现,注意可能会有一些问题,首先定时任务是把分散的超时订单集中在任务调度那一刻处理,所以时间订单处理时间可能就不精准,而且订单量越大,那么调度任务执行时间长的话时间偏差也就越大,其次大数据量时,定时集中扫表,需要考虑数据库压力,不能影响正常业务。但是一般来说,订单超时关闭这种业务,对时间精确度并不是要求很高,所以需要考虑的主要是定时扫表性能问题。
大数据量库表读表性能问题,首先可以加索引,在state(状态)字段上加索引,这边会牵扯出一个问题,那就是区分度不高的字段加索引是否有用,实际上如果该字段对应的枚举占的比例相差很大,比如订单状态,成功的可能占90%,需要关闭的占10%,这样的我查询这部分需要关闭的数据,他可以过滤掉大部分数据,走索引还是可以大大提升效率的。其次可以考虑使用多线程执行,这种方案要注意的就是做好数据隔离,别重复处理一条数据,处理方案可以是扫表后处理时注意幂等控制(一次和多次请求某一个资源应该具有同样的作用),也可以采用分段处理。最后可以使用备库,扫描备注然后根据id直接去修改主库。
4、MQ方案实现延迟消息
4.1 RocketMQ延迟消息:消息写入Broker之后不会立即被消费,而是在指定时间后才会被消费处理。存在无法自由设置关单时长的问题,RocketMQ并不支持任意时长设置(商业版和最新的5.0版本可支持任意时长)。
4.2 RebbitMQ死信队列:给消息设置TTL但是并不消费,到期后消息进入死信队列(交换机-exchange),监听死信队列的消息进行消费。存在问题:死信队列的对头消息阻塞;实现方案麻烦(依赖RebbitMQ,要创建许多exchange)。
4.3 RebbitMQ插件:基于rebbitmq_delayed_message_exchange插件,消息不是通过死信队列处理,而是直接存入Erkang开发的一个数据库,定时查询需要投递的消息进入x_delayed_message队列中,所以不存在消息阻塞的问题,但是要注意有延迟时间限制,这个插件最长支持大约49天的时间。
***注意:***不建议使用MQ,一是订单量大就会堆积大量的消息,资源浪费,成本提升,而且需要注意的是这里面还会有大量的无效消息,因为大部分订单可能是提前取消和完成支付的;二是延迟时间的限制,特别是B类采购订单长关单期很难满足,另外很重要的是,MQ还是会有丢消息的风险的。
5、redis
5.1 消息监听::配置文件redis.conf中开启监听:notify-keysppace-events Ex,代码中实现KeyExpirationEventMessageListener,不推荐使用,过期的key不能保证被立即删除,也不能保证能立即发出,另外还可能存在消息丢失问题。
5.2 zset:socre设置时间戳+超时时间,member为订单号,zset会按照score排序,开启redis扫描任务,获取当前时间>score的延时任务,拿到订单号进行关单。高并发场景下可能会有多个消费者拿到同一个订单的现象。
6、Reddisson
Reddisson定义了分布式延迟队列RDelayedQueue,RDelayedQueue.offer()方法将消息放入RDelayedQueue,到期后Reddisson会将元素从RDelayedQueue转移到RBlockingDeque,RBlockingDeque.take()方法获取元素。
大量登录请求,JVM调优
登录接口一般并不会携带太多信息,高并发环境下产生的众多小对象会很快被回收,考虑JVM的调优主要就是内存的配置和垃圾收集器(吞吐量和STW)的选择,同时做好GC日志的监控。
1、堆内存设置
一般设置为操作系统内存的一半,比如一台4C8G的机器,那么就设置为4G,初始内存和最大内存建议都为4G,避免内存的频繁扩容和收缩。
-Xms4G -Xmx4G
2、垃圾收集器
新生代频繁GC,兼顾高吞吐量和较短的STW(停顿时间,JVM在执行垃圾回收过程中,所有Java执行线程被暂停的持续时间),考虑使用G1作为垃圾收集器(JDK 9 默认,需要内存最少要4G)。
-XX:+UseG1GC
-XX:MaxGCPauseMillis=150
MaxGCPauseMillis为设置的停顿时间,JVM执行时会尽量在设置的这个时间内完成回收,可以使用jstat查看实际停顿时间,如果实际停顿时间远大于这个设置的时间,就需要重新进行一些配置。
另外,可以设置GC的并发线程数
-XX:ParallelGCThreads=4
-XX: ConcGCThreads=2
3、各区大小设置
G1的内存划分是动态自适应的,但是也可以手动进行一些配置
-XX:G1HeapReagionSize=2m
-XX:G1NewSizePercent=20 //年轻代的初始大小为堆的20%
-XX:G1MaxNewSizePercent=50 //年轻代的最大大小为堆的50%
-XX:G1OldCSetRegionThresholdPercent=10 //老年代的大小为堆的10%
-XX:G1HeapWastePercent=10 //垃圾回收后留下的未使用区域的最大比例为10%
4、日志输出
以上配置还需要根据具体情况进行调整,所以增加日志来观察调整
-XX:+HeapDumpOnOutOfMemoryError //内存溢出时输出快照文件
-XX:HeapDumpPath=/path/to/dump.hprof //堆内存快照文件的存储路径
-XX:+PrintGC //输出GC信息
-XX:+PrintGCDateStamps //输出GC发生时间
-XX:+PrintGCDetails //输出GC详细信息
-Xlog:gc*=info:file=/path/to/logs/gc.log:time,uptime:filecount=10,filesize=100M //gc日志输出路径,设置文件大小和数量
业务请求量突增怎么处理
面对流量的激增的问题要注意区分情况,首先注意甄别是否是正常场景。非正常流量激增可能是被DDOS了(攻击者利用大量“肉鸡”发起大量的正常或非正常的请求,耗尽主机资源或网络资源,从而使被攻击的主机无法为正常用户提供服务,被DDOS时被攻击主机上会有大量等待状态的TCP连接,网络中充斥着大量的无用数据包,主机无法正常与外界通讯并提供服务甚至直接系统死机,在用户看来就是网站无法访问)。而正常的流量提升需要考虑两个方面:一方面是长期的可预期的业务好转,施行长期方案,即设计一个支持高并发的系统,包括架构、性能优化和容错机制等等。另一方面就是短期的热点事件影响,这时短期的简单处理就是扩容,增加集群的服务器数量,提升机器硬件配置等。
重点说下一个高并发系统相关的设计和优化要点:
1、分布式架构
将系统拆解成不同的功能模块部署在不同的机器上,相互之间通过远程调用协同工作,对外提供服务。
2、集群部署
在不同的机器上部署相同的应用或者功能模块,通过负载均衡设备对外提供服务。
3、利用缓存
Redis、Ehcache缓存、NoSql技术,提高数据访问性能
4、异步处理、消息队列
消息队列:解耦、异步、削峰填谷,减少请求响应时间,提高系统吞吐量。
kafka支持发布-订阅模式,每秒可处理百万级数据,使用更简单,当然一些高级功能就需要比较麻烦的配置;ActiveMQ/RoketMQ/RabitMQ同时支持点对点和发布-订阅模式,吞吐量相对小点,功能更多样灵活,但相应的使用起来更复杂。
5、预加载
浏览器提前下载某些资源,减少用户的等待时间。
6、代码优化
6.1、单例
IO处理、数据库连接、配置文件解析等使用单例模式
6.2、批量
如涉及数据库的操作,批量执行
6.3、Future模式
通过Future对象异步获取获取返回值,同时主流程继续处理其他业务逻辑
6.4、使用线程池
6.5、锁优化
减少锁的持有时间(同步代码块);减小锁粒度(如ConcurrentHashMap的分段锁)
6.6、缓存结果
本地缓存或者分布式缓存储存计算结果或者查询结果,减少重复的数据库查询和磁盘IO
6.7、SQL优化
6.7.1 索引失效
针对慢sql,通过explain查看执行计划,注意type、key和extra字段,分别是索引类型,使用的索引和查询时的附加操作,三者结合来看可以看出是否使用了索引,如果有走索引,判断是否走了覆盖索引或者是否全扫描索引树等等。简单来说,key要有值,不能是NULL,type应该是ref、eq_ref、range、const等这几个,extrausing index、using index condition都是可以的。
失效原因:
(1)条件字段没有索引或者不满足最左前缀匹配
(2)索引区分度不高,可能会不走索引
(3)表数据少,直接全表扫描
(4)查询语句中,索引字段用了函数计算或者数据类型不一致等
上述(4)包含sql语句的问题补充如下:
(a)MySql用了函数计算之后索引不是一定会失效,MySql 8.之后引入函数索引。
(b)sql语句使用OR,并且OR两边使用<或>,如果OR两边使用的=的话还是可以走索引的。
(c)sql语句使用LIKE,%在 字符串的首位则不走索引。
(d)隐式类型转换,varchar类型字段查询时使用int类型,索引失效,但是注意int类型字段查询时加了单引号或者双引号,参数会自动转换为int类型,能走索引。
(e)sql语句使用 != ,并不是绝对不走索引,比如用自增主键ID时就可能走索引,这个要看索引的选择和数据分布情况。
(f)sql语句使用 is nt null。
(g)sql语句使用 order by,数据量很小时,直接在内存中排序,不使用索引。
(h)sql语句使用 in, in的值比较多的时候可能就不走索引 了。
6.7.2 多表join或者查询字段太多
MySql的嵌套查询效率较低,如果不用join可以考虑代码作二次查询再进行数据关联处理,或者设计表的时候允许数据冗余或基于join关系做宽表。
6.7.3 数据库连接数不够
常见于热点数据更新,多个update语句会排队获取锁,占用连接资源 ,解决思路有1、基于缓存做数据更新,如redis,2、异步更新或者批量更新
6.7.4 数据库IO或者CPU比较高
6.7.5 深度分页问题
考虑使用子查询以及记录上一页ID的方案
6.8、 数据库优化
6.8.1 合理的数据库索引
6.8.2 分库分表
分库针对并发量大,数据库连接数不够。
分表针对表数据量大,查询性能受到很大影响。
拆分分横向拆分和纵向拆分,横向拆分就是把表中不同的记录放到不同的表中,纵向拆分就是把某条记录的多个字段拆分到不同的表中。
分库分表工具:sharing-jdbc,TDDL,MyCat
6.8.3 读写分离
读请求到从库,写请求到主库,主从库通过主从复制实现数据同步,具体读写分流实现方法有:
(a) 代码分流,在DAO层定义定义多个数据源,在实际进行读或者写操作时利用AOP在业务层或者DAO层方法调用前动态切换数据源。
(b) 借助中间件,sharing-jdbc,TDDL等均支持读写分离
注意问题:主从延迟,针对这个问题可以做一些优化,对于不能接受延迟的读请求,强制读主库。
6.9、限流、熔断、降级
6.10、容错和监控
6.11、全面的性能测试和评估
压力测试和安全测试