学习笔记(7)redis缓存
缓存设计
限流的理念是“多层限流、尽早限流”,缓存也是如此。缓存是利用“数据冗余”来阻挡海量请求的冲击,即缓存实际是同一份数据的不同形式拷贝。例如,数据库缓存Redis实际就是以key-value结构,来存储数据库中二维表结构的数据。Redis中的数据,实际就是数据库的一份冗余。
在设计缓存时,需要明确一点:对于数据的访问,“读的次数”远远大于“写的次数”。
缓存动态请求
在秒杀活动期间,大量的用户会访问相同的商品。因此可以在秒杀开始前,提前在后台服务中将参与秒杀的商品缓存到内存中,从而减少大量的请求流入数据库中,如图所示。
在秒杀期间,也可以动态缓存频繁使用的各种数据。缓存几乎是所有并发系统的标配之一。
在缓存动态请求时,还需要考虑缓存击穿、缓存雪崩和缓存穿透等问题。
- 缓存击穿:如果某一个热点数据的缓存过期了,那么当大量用户同时访问这个热点数据时,就会导致数据库在缓存过期的瞬间压力突然增大;
- 缓存雪崩:如果大量数据的缓存同时失效(可能是给大量缓存设置了相同的过期时间,也可能是缓存服务器出现故障等),那么全部的后台请求将直接奔向数据库,从而导致数据库压力过大;
- 缓存穿透:一般而言,我们会将那些频繁查询的数据放入缓存,并不会缓存毫无意义的数据。但这也给一些不怀好意的人带来了可乘之机:如果有人恶意大量查询一些不存在的商品,那么这些查询请求就会绕过缓存而直达数据库,因此也会给数据库带来很大压力。
缓存击穿、缓存雪崩和缓存穿透都是由于某种情况下的“缓存失效”所导致的。三者也都有各自的解决方案,举例如下:
- 避免缓存击穿:通过一个线程实时监控热点数据的过期时间,如果发现某个缓存快要过期了,就开启一个异步线程去更新缓存,从而重置过期时间。或者也可以简单一点:在秒杀开始前,手工的设置热点数据在秒杀期间不会过期。;
- 避免缓存雪崩:搭建缓存集群(如 Redis 集群),并合理的分配缓存的过期时间;
- 避免缓存穿透:对于不存在的商品,也将其以value="“的形式进行缓存(例如, key =“不存在的商品”, value =”")。这样一来,当以后再次查询“不存在的商品”时,也能迅速的从缓存中查到结果(结果就是"")。此外,为了防止大量无效数据长时间占用缓存容量,可以将这些无意义缓存的过期时间设置的短一些。
多级缓存
“限流”可以使用 nginx ,也可以使用 lvs + nginx 等多种组合,缓存也可以有多级。
对于后台服务,可以有多级缓存。例如,可以先在本地使用 ConcurrentHashMap 、 GuavaCache 、 tomcat 缓存等作为一级缓存,然后搭建 Redis 集群作为远程的二级缓存。如果必要,还可以再在进程内增加一层缓存。
显然,缓存的级数越多,抵达数据库的请求数就越少,整个系统的压力也会随之减少。但利弊总是同时存在,缓存会造成数据的一致性问题,缓存的级数越多,一致性问题就会越严重;并且多级缓存自身也会对系统造成一定的压力,例如本地缓存需要通过网络调用远程缓存,因此既会增大网络带宽,也会增加整个系统内部的调用次数。对于秒杀抢购来说,一种推荐的缓存结构是“客户端本地缓存(如浏览器缓存) + nginx(存储到 OSS )+ CDN + 本地缓存 + redis 远程缓存”,如图所示:
对于数据库缓存来说,既然缓存的读写速度要远大于数据库的速度,并且很多缓存自身也支持持久化功能,那么能否在秒杀活动期间仅仅使用数据库缓存,而不用数据库本身。当秒杀结束后再将缓存中的数据同步到数据库中,从根本上解决秒杀期间数据库读写速度慢的问题?
缓存组件的设计目标是“快”,因此在设计时为了“快”而放弃了一些其它功能。例如最常用的数据库缓存 Redis 就不支持事务回滚,如果遇到了需要进行回滚的业务,那么 Redis 就无法支持了。当然,也可以通过日志处理等手段间接实现回滚功能,但这无疑又增大了系统的开发成本和性能损耗(日志的读写操作会损耗性能)。因此,架构设计时经常说的一句话“根据具体业务、具体分析,没有统一的结论”。
缓冲区
与缓存比较相似的一个概念是“缓冲区”。缓存一般是 Redis 等中间件,是数据库等内容的一部分拷贝,用于缓解数据库等目标服务的压力,可以理解为一个无序的容器,没有 FIFO 等特性;而缓冲区一般是内存中的一块区域(如数组),用于解决生产端和消费端速度不一致的问题,遵循着 FIFO 等规则。
缓冲区几乎存在于所有的高并发技术之中,常见的缓冲区有数组和环形缓冲区两种形式,此外环形缓冲区实质也是一个首尾相连的数组,如图所示:
知名的分布式计算框架 MapReduce 、单线程每秒吞吐量可达六百万以上高并发框架 disruptor 以及去哪儿网开源的消息队列 QMQ 等,都广泛使用了环形缓冲区。而另一些并发框架(如 netty ),以及 nio 等技术则使用了普通的数组作为缓冲区。
在设计秒杀系统时,如果遇到后台中多个服务之间的处理速度不一致时,就可以考虑使用缓冲区进行协调。例如,在秒杀系统中,“下订单”的速度要明显快于“处理订单”的速度,因此就可以使用缓冲区平衡二者的速度:生产者(下订单服务)快速的将数据存入缓冲区中,与此同时,消费者(处理订单服务)根据自身的速度从缓冲区中获取数据。读到这里,大家应该能够发现之前介绍的消息队列本质也属于缓冲区的一种。
淘汰缓存
在秒杀等高并发系统中,缓存的功能是必不可少的。但是由于内存容量的限制,当缓存达到一定规模时,就有必要通过一些策略及时淘汰部分缓存,防止内存溢出等异常情况。常见的淘汰策略可以是基于缓存数量或缓存容量,并配合 LRU 等算法使用。但在 Java 中,还可以使用本节所讲的引用来实现缓存的淘汰策略。
缓存的使用无外乎 get 和 set,在一般情况下,我们可以把要缓存的对象 set 到 HashMap 等容器中,然后在使用时直接从容器中 get 即可。但是如果要缓存的对象本身很大,那么同时将多个大对象进行缓存就会给内存带来巨大挑战。使用软引用解决这一问题,因为软引用会在内存不足时被 GC 及时回收,具体如下代码所示。
class BigValue {
...
//模拟大容量对象
}
public class ReferenceCache {
Map<String, SoftReference<BigValue>> caches = new HashMap();
//根据id存储缓存对象(缓存对象被装饰在了软引用中)
void setCache(String id, BigValue bigValue) {
caches.put(id, new SoftReference<BigValue>(bigValue));
}
//根据id获取缓存对象
BigValue getCache(String id) {
//根据id,获取缓存对象的软引用
SoftReference<BigValue> softRef = caches.get(id);
return softRef == null ? null : softRef.get();
}
}
总结
缓存与“限流”虽是不同的技术,但二者的使用思路基本一致:都是以请求的“时间线”为线索。在分析多级缓存时,应该分析请求路径中的每个阶段能否加入缓存、如何加入缓存、加入缓存带来的利弊该如何权衡。
在设计多层限流、多级缓存时,切忌进行盲目的技术堆积,而要清晰认识每一级缓存的作用。对于动态请求来说,“ Redis 远程缓存”可以作为数据库的缓存,而 GuavaCache 等“服务器本地缓存”主要用于保护远程缓存(例如可以减少 Redis 失效造成的缓存雪崩等问题)。
缓存具体实现
为了系统性能的提升,我们一般都会将数据放入缓存中,加速访问。而db承担数据落盘工作。
高并发系统,缓存是必备。
缓存的使用场景:
一些固定的数据,不太变化的数据,高频访问的数据(基本不变),变化频率低的都可以入缓存,加速系统的访问。
缓存的目的:提高系统查询效率,提供性能
1)、将菜单缓存起来,以后查询直接去缓存中拿即可;
设计模式:模板模式:
操作xxx都有对应的xxxTemplate;
JdbcTemplate、RestTemplate、RedisTemplate、MongoTemplate
RedisTemplate<Object, Object>; k-v;
v有五种类型、String、V
StringRedisTemplate: k-v都是String的。
引入一个场景,猜这个场景的xxxAutoConfiguration,帮我们注入能操作这个技术的组件,这个场景的配置信息都在xxxProperties中说明了(prefix = spring.redis")使用哪种前缀配置
2)、整合Redis两大步
1)、导入starter-data-redis
2)、application.properties配置与 spring.redis相关的
注意:
RedisTemplate;存数据默认使用jdk的方式序列化存过去。
我们推荐都应该存成json;
做法:
将默认的序列化器改为json的
spring-boot把每个功能都抽取成场景启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
会有RedisAutoConfiguration,往容器中注入template
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
/**
* 只需要自己创建出自己满意的序列化器放入容器中即可
*/
//默认的RedisTemplate;存数据默认使用jdk的方式序列化存过去。
我们推荐都应该存成json;(方便阅读)
做法:
将默认的序列化器改为json的
//@Bean标注的所有方法都会在IOC容器自己获取
@Configuration
public class PmsRedisConfig {
/**
* jedis
* @param redisConnectionFactory
* @return
* @throws UnknownHostException
*/
@Bean("redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//修改默认的序列化方式
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}