正文
给一些没有必要长期保存的key,尽量设置过期时间。
缓存是否允许丢失?丢失了怎么办?
根据业务场景判断,是否允许丢失。如果不允许,就需要带持久化功能的缓存服务来支持,比如Redis或者Tair。更细节的话,可以根据业务对丢失时间的容忍度,还可以选择更具体的持久化策略,比如Redis的RDB或者AOF。
缓存被“击穿”问题
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑另外一个问题:缓存被“击穿”的问题。
-
概念:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
-
如何解决:业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。类似下面的代码:
异步
使用场景
针对某些客户端的请求,在服务端可能需要针对这些请求做一些附属的事情,这些事情其实用户并不关心或者用户不需要立即拿到这些事情的处理结果,这种情况就比较适合用异步的方式处理这些事情。
作用
-
缩短接口响应时间,使用户的请求快速返回,用户体验更好。
-
避免线程长时间处于运行状态,这样会引起服务线程池的可用线程长时间不够用,进而引起线程池任务队列长度增大,从而阻塞更多请求任务,使得更多请求得不到技术处理。
-
线程长时间处于运行状态,可能还会引起系统Load、CPU使用率、机器整体性能下降等一系列问题,甚至引发雪崩。异步的思路可以在不增加机器数和CPU数的情况下,有效解决这个问题。
常见做法
一种做法,是额外开辟线程,这里可以采用额外开辟一个线程或者使用线程池的做法,在IO线程(处理请求响应)之外的线程来处理相应的任务,在IO线程中让response先返回。
如果异步线程处理的任务设计的数据量非常巨大,那么可以引入阻塞队列BlockingQueue作进一步的优化。具体做法是让一批异步线程不断地往阻塞队列里扔数据,然后额外起一个处理线程,循环批量从队列里拿预设大小的一批数据,来进行批处理(比如发一个批量的远程服务请求),这样进一步提高了性能。
另一种做法,是使用消息队列(MQ)中间件服务,MQ天生就是异步的。一些额外的任务,可能不需要我这个系统来处理,但是需要其他系统来处理。这个时候可以先把它封装成一个消息,扔到消息队列里面,通过消息中间件的可靠性保证把消息投递到关心它的系统,然后让这个系统来做相应的处理。
比如C端在完成一个提单动作以后,可能需要其它端做一系列的事情,但是这些事情的结果不会立刻对C端用户产生影响,那么就可以先把C端下单的请求响应先返回给用户,返回之前往MQ中发一个消息即可。而且这些事情理应不是C端的负责范围,所以这个时候用MQ的方式,来解决这个问题最合适。
NoSQL
和缓存的区别
先说明一下,这里介绍的和缓存那一节不一样,虽然可能会使用一样的数据存储方案(比如Redis或者Tair),但是使用的方式不一样,这一节介绍的是把它作为DB来用。如果当作DB来用,需要有效保证数据存储方案的可用性、可靠性。
使用场景
需要结合具体的业务场景,看这块业务涉及的数据是否适合用NoSQL来存储,对数据的操作方式是否适合用NoSQL的方式来操作,或者是否需要用到NoSQL的一些额外特性(比如原子加减等)。
如果业务数据不需要和其他数据作关联,不需要事务或者外键之类的支持,而且有可能写入会异常频繁,这个时候就比较适合用NoSQL(比如HBase)。
比如,美团点评内部有一个对exception做的监控系统,如果在应用系统发生严重故障的时候,可能会短时间产生大量exception数据,这个时候如果选用MySQL,会造成MySQL的瞬间写压力飙升,容易导致MySQL服务器的性能急剧恶化以及主从同步延迟之类的问题,这种场景就比较适合用Hbase类似的NoSQL来存储。
JVM调优
什么时候调?
通过监控系统(如没有现成的系统,自己做一个简单的上报监控的系统也很容易)上对一些机器关键指标(gc time、gc count、各个分代的内存大小变化、机器的Load值与CPU使用率、JVM的线程数等)的监控报警,也可以看gc log和jstat等命令的输出,再结合线上JVM进程服务的一些关键接口的性能数据和请求体验,基本上就能定位出当前的JVM是否有问题,以及是否需要调优。
怎么调?
-
如果发现高峰期CPU使用率与Load值偏大,这个时候可以观察一些JVM的thread count以及gc count(可能主要是young gc count),如果这两个值都比以往偏大(也可以和一个历史经验值作对比),基本上可以定位是young gc频率过高导致,这个时候可以通过适当增大young区大小或者占比的方式来解决。
-
如果发现关键接口响应时间很慢,可以结合gc time以及gc log中的stop the world的时间,看一下整个应用的stop the world的时间是不是比较多。如果是,可能需要减少总的gc time,具体可以从减小gc的次数和减小单次gc的时间这两个维度来考虑,一般来说,这两个因素是一对互斥因素,我们需要根据实际的监控数据来调整相应的参数(比如新生代与老生代比值、eden与survivor比值、MTT值、触发cms回收的old区比率阈值等)来达到一个最优值。
-
如果发生full gc或者old cms gc非常频繁,通常这种情况会诱发STW的时间相应加长,从而也会导致接口响应时间变慢。这种情况,大概率是出现了“内存泄露”,Java里的内存泄露指的是一些应该释放的对象没有被释放掉(还有引用拉着它)。那么这些对象是如何产生的呢?为啥不会释放呢?对应的代码是不是出问题了?问题的关键是搞明白这个,找到相应的代码,然后对症下药。所以问题的关键是转化成寻找这些对象。怎么找?综合使用jmap和MAT,基本就能定位到具体的代码。