一、扩容
1.1、扩容的两个方式
1.1.1、垂直扩容(纵向扩展)
提高系统部件能力(从系统现有部件下功夫,通过精细计算处理,将系统部件的效率提高)
1.1.2、水平扩容(横向扩展)
增加更多系统成员来实现(增加服务器,服务器集群)
1.2、数据库的扩容
1.2.1、读操作扩展
memcache、redis、CDN等缓存
1.2.2、写操作扩展
(可水平扩展的数据存储方式,作用并没有像读操作中的那样强大)Cassandra、Hbase等。很多写频繁的系统,一开始使用垂直扩展的方式,但是很快的发现并不能根本解决问题,硬盘数和处理器数在某一点达到了平衡,在这个边界上,再增加一个处理器或者一个硬盘,作用都不大。这种情况下,增加节点会更实惠
二、缓存
2.1、缓存的特征
2.1.1、命中率:命中数/(命中数+没有命中数)
命中是直接通过缓存获取到需要的数据。
没有命中是无法直接通过缓存获取到数据,需要再次查询数据库或者执行其它的操作。
2.1.2、最大元素(空间)
缓存中可以存放的最大元素的数量,超过则会触发缓存清空策略。
2.1.1、清空策略:FIFO,LFU,LRU,过期时间,随机等
(1)FIFO
先进先出策略,最先进入缓存的数据,在缓存空间不够的情况下或者是超出最大元素限制的时候会优先被清除掉。主要比较的是缓存元素的创建时间。在数据实时性要求的场景下,可以选择这类策略。
(2)LFU
最少使用策略,无论是否过期,根据元素的被使用次数来判断,清除使用次数最少的元素。主要比较的是命中次数。在保证高频数据有效性的场景下,优先选择这类策略
(3)LRU
最近最少使用策略,指无论是否过期,根据元素最后一次被启动的时间戳,清除最远使用时间戳的元素,主要比较元素的最近一次被get的时间,在热点数据场景下适用,优先保证热点数据的有效性。
(4)过期时间
根据过期时间来判断,清理过期时间最长的元素,还可以根据过期时间判断最近要过期的元素
(5)随机
随机清理
2.2、缓存命中率的影响因素
2.2.1、业务场景和业务需求
- 主要适合读多写少的应用场景,实时性要求越低就越适合缓存,在相同key或相同请求数的情况下,缓存的时间越长,缓存命中率就越高。目前大多数都适合缓存。
2.2.2、缓存的设计(粒度和策略)
- 粒度越小命中率越高,当数据发生变化的时候才会更新缓存或者移除缓存,当缓存一个集合的时候,其中任何一个对象的数据发生变化时,我们都需要更新或移除缓存。
2.2.3、缓存容量和基础设施
- 缓存容量有限,就容易引起缓存失效和被淘汰,目前多数的缓存框架或中间件都采用了LRU算法。
- 缓存的技术选型也是非常重要的,比如采用应用内置的本地缓存就比较容易出现单机瓶颈,而采用分布式缓存,就更容易扩展,所以需要做好系统容量规划,并考虑是否可扩展。
- 不同的缓存框架或中间件的效率和稳定性也是存在一些差异的。
- 当缓存节点发生故障的时候需要避免缓存失效并最大程度的降低影响,可以通过一致性哈希算法或者通过节点冗余的方法来避免这个问题。
2.3、缓存的使用
- 并发越高,缓存的收益就越高
- 架构师要应用缓存来直接的获取数据,并避免缓存失效,这也是比较考验架构师能力的,要在业务需求,缓存粒度,缓存策略,技术选型等各个方面去通盘考虑并做权衡,尽可能的聚焦在高频访问且时效性要求不高的热点业务上,通过缓存预加载增加存储容量,调整缓存力度,更新缓存等手段来提高命中率。
三、缓存分类和应用场景
3.1、本地缓存
3.1.1、本地缓存类型
- 编程实现(成员变量、局部变量、静态变量)
- Guava Cache
3.1.2、本地缓存是什么
- 指的是应用中的缓存组件,最大的优点是应用和cache是在同一个进程的内部,请求缓存非常的快速,没有过多的网络开销等等,在单应用中不需要集群支持。
- 集群情况下,各节点不需互相通知的情况下使用本地缓存比较合适。
- 缺点是因为缓存跟应用耦合,多个应用程序无法直接共享缓存,各应用和各个单独的节点都需要维护自己的单独的缓存,有时对内存会有一定的浪费
3.1.3、本地缓存-Guava Cache
- 谷歌开源的Java工具库
- 左侧是结构图,继承了ConcurrentHashMap的思路
- LRU算法移除数据
3.2、分布式缓存
- Memcache、Redis
- 应用分离的缓存组件或服务,最大的优点就是自身就是一个独立的应用,与本地应用是隔离的,多个应用可以直接共享缓存。
3.2.1、Memcache缓存
一个page是1M。chunk是真正存放数据的地方,chunk里面总会有内存浪费。LRU方法不是针对全局的,是针对slab的。因为page是1M所以value的大小不能大于1M。
限制:只要内存足够,保存的item数量没有限制,Memcache单进程在32位机中最大使用的内存是2G,64位机没有这个限制。key最大为250个字节,超过就没办法存进去,page的最大容量也是1M,超过也是没办法存进去的。Memcache的服务器端是不安全的。不能遍历里面存储的所有item,因为这个操作的速度相对缓慢,而且会阻塞其它的操作。
特性:
它的高性能来源于两个阶段的hash结构
第一个阶段在客户端,通过哈希算法根据key值算出一个节点,第二阶段是在服务端,通过一个内部的哈希算法查找真正的item并返回给客户端,从时间的角度看Memcache是一个非阻塞的基于事件的服务器程序,它在设置某一个key值的时候,可以传入一个值为0,保持这个key永久有效,但是这个值会在30天之后失效,这是源码实现
3.2.3、Redis缓存
(1)Redis是什么
- 远程的内存数据库,非关系的数据库
- 存储键值对以及五种不同类型的值之间的映射。可以将存储在内存的键值对数据持久化到硬盘。
(2)Redis的特点
- 支持数据的持久化,可以将内存中的数据保存在磁盘里,重启的时候会再次加载进行使用。
- redis除了支持普通的key-value之外,还有list等五种数据都支持。
- 性能极高,读的速度能到11万次每秒,写的速度可以到8万1千次每秒
- 有丰富的数据类型。
- 具有原子性
- 可以自定义取哪些数据,比如取最新n个数据,排行榜等数据,或者精准设计过期时间,也可以用于计数器的使用,可以适用于做唯一性检查的操作,实时消息系统、队列系统、最基础的缓存功能
四、高并发场景下缓存的常见问题
4.1、缓存一致性
4.1.1、缓存一致性是啥
数据时效要求高,则必须保证缓存中的数据和数据库中的数据保持一致,也要保证缓存节点和副本中的数据也保持一致,这就比较依赖缓存的过期和更新策略了。一般会在数据发生更改的时候,主动更新缓存中的数据或者移除对应的缓存,这个时候就可能会出现缓存一致性的问题
4.1.2、缓存一致性的四种情况
4.2、缓存并发问题
缓存过期后,将尝试从后端的数据库获取数据,这是一个看似合理的过程,但是在高并发场景下,又可能多个请求并发的去从数据库中获取数据,对后端数据库造成极大的冲击,甚至导致雪崩的现象,此外当某个缓存的key被更新时,同时可能被大量请求获取,这也会导致一致性的问题。类似于锁的机制可能会缓解这个情况。
4.3、缓存穿透问题
4.3.1、缓存穿透出现的情况
在高并发场景下,如果某一个key被高并发的访问,没有被命中,处于对容错性的考虑,会尝试去从后端数据库去获取,从而导致了大量的请求达到了数据库,而当该key对应的数据本身,就是空的情况下,这就导致数据库中并发的去执行了很多不必要的查询操作,从而导致了巨大的冲击和压力,这种情况可以通过后面几种常用的方式来问题。
4.3.1、避免缓存穿透
① 缓存空对象:对查询结果为空的对象也进行缓存,如果是集合的话,可以缓存一个空的集合,但不是空null,如果是缓存单个对象,可以通过字段标识来区分,这样避免请求穿透到后端数据库,同时需要保证缓存数据的时效性,这种方式实现起来成本较低,比较适合命中不高,但可能被频繁更新的数据。
② 单独过滤处理,对所有可能对应数据为空的key进行统一的存放,并在请求前做拦截,这样避免请求穿透到后端数据库,这种方式实现起来相对复杂一些,比较适合命中不高,但是更新不频繁的数据。
4.4、缓存雪崩现象
- 缓存的颠簸(抖动)问题,是一个比雪崩轻微的故障,但是也会在一段时间内对系统造成冲击和性能影响,一般是由于缓存节点故障导致,业内推荐的做法是通过一致性哈希算法解决。
- 缓存雪崩是指由于缓存的原因导致大量的请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃,发生灾难。原因有很多,比如缓存并发、缓存穿透、缓存颠簸(抖动)。
- 以上这些问题也可能会被恶意攻击者利用。
- 还有就是部分缓存周期性集中的失效也可能会导致雪崩,为了避免这种周期性的失效可以设置不同的过期时间,来错过开他们的缓存过期时间,从而避免缓存集中失效,从应用架构角度可以通过限流、降级、熔断等手段降低影响,也可以通过多级缓存来避免这种灾难。多加强压力测试。
五、缓存高并发实战、股票分时线
百度搜索:redis在股票分时K线图计算的实践
之后会将本思路运用在我校内接的商业项目上,并将实现代码展示以便学习巩固。
- 用Javacache缓存最近的所有数据,key为数据的时间,单位为分钟,一分钟之内有多次修改数据,则覆盖,每分钟只有一个数据。
- 然后启动一个定时任务,每分钟将最近几分钟的数据都写入到redis里面,保证redis里面的数据一直都是最新的。
- redis使用了hash结构,key为小时和分钟,用户想看某个时间段的,就只取某个时间段大数据并做成map,计算好需要展示的点,后端会先处理redis取出来的数据,因为有可能会出现某一段时间没有数据的情况,缺少的点就需要用这个点的前一个点进行补充。
六、使用Redis
6.1、开启Redis服务
6.2、导入Redis依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
6.3、配置Redis信息
jedis:
host: 127.0.0.1
port: 6379
6.4、RedisConfig
package com.tangxz.redis;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
/**
* @Info:
* @Author: tangxz
* @Date: 2019/12/29 23:35
*/
@Configuration
public class RedisConfig {
@Bean(name = "redisPool")
public JedisPool jedisPool(@Value("${jedis.host}") String host,@Value("${jedis.port}") int port){
JedisPool jedisPool = new JedisPool(host,port);
return jedisPool;
}
}
6.5、RedisClient
package com.tangxz.redis;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import javax.annotation.Resource;
/**
* @Info: redis客户端
* @Author: 唐小尊
* @Date: 2019/12/29 23:40
*/
@Component
public class RedisClient {
@Resource(name = "redisPool")
private JedisPool jedisPool;
public void set(String key,String value) throws Exception{
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
jedis.set(key,value);
}finally {
if (jedis!=null){
jedis.close();
}
}
}
public String get (String key) throws Exception{
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
return jedis.get(key);
}finally {
if (jedis!=null){
jedis.close();
}
}
}
}