缓存的应用 : 缓存——工程思想的产物

缓存一词最初主要指 CPU 与内存之间的高速静态随机存取存储器(SRAM)。

一 web中的缓存

在做项目的过程中,不知道你们有没有感叹过,一个平平无奇的应用,涉及的点实在是太多了。各个点之间需要衔接,要衔接就会有两个层次的不均衡:

     一是性能的不均衡,包括速率、吞吐量等,造成这种不均衡的原因包括软件、硬件、网络、协议、策略等、位置多个维度。
     二是数据本身活跃性的不均衡,有些数据会被频繁传递,有些很久才被访问一次。

基于这两个不平衡,诞生了各种缓存方案。比较常见的有以下几种:

浏览器缓存,包括本地的页面资源文件和 DNS 映射
DNS 服务器上的缓存(IP - 域名映射)
CDN,利用边缘 Cache 服务器提高访问速度
ORM 框架提供的缓存,比如 Spring Data JPA 的持久化上下文
利用高性能非关系型数据库(如 Redis)提供缓存服务,作为对关系型数据库的补充
数据库提供的缓存,比如 MySQL 自带的查询缓存,会把执行语句与查询结果以 K-V 形式缓存在内存中(由于该缓存命中率较低,不建议使用,且 8.0 版本已删除此功能)

不得不说,对成熟的应用来说,一个普通的请求想过了缓存这关还真不容易。

看起来缓存还真是个好东西,到哪都好用。但多用了一个东西,毕竟还是会增加复杂性,复杂性越高越不好控制,我们设计一个软件的架构,就是要让它在够用的前提下尽可能简单,实现简单、控制简单、维护简单

1 缓存的工作模式

 1.1   Cache-Aside:

    最常见的模式,可以翻译为旁路缓存或边缘缓存。缓存作为数据库(或存储)的补充,数据的获取策略是,如果缓存中存在,则从缓存获取,如果不存在,则从数据库获取,并写入缓存。

 

1.2 Read-Through:( )

      把数据库藏在缓存背后,一切请求交由缓存响应。也就是说,如果命中缓存,则直接从缓存获取,如果没有命中,则从数据库中查询,写入缓存后再由缓存返回。

应用这种模式,写入缓存的操作会阻塞请求的响应,我觉得其实大部分情况下没有必要使用。

1.3  Write-Through:(请求更新数据,如果该数据在缓存中存在,则先更新缓存,再更新数据库。

1.4   Write-Back:

       请求更新数据,更新缓存,至于数据库什么时候更新,不一定,有机会再更新,可以攒一波再更新,有缓存在就行。

      这种异步的方式一听就有数据不一致的风险,但因为够快,所以在一些要求高并发大吞吐量的系统中比较常见。其实高并发的一个核心解决方案就是缓存,高并发的复杂性很大程度上取决于缓存方案的复杂性。

这些方案具体怎么用其实还是看场景,要配置相应的策略防止出现一些问题。

2. 2.缓存的常见问题

   在使用缓存时,我们一般都会考虑以下几个问题:

  • 数据一致性问题,缓存的数据与数据库由于各种原因产生差异
  • 缓存穿透,明明已经用缓存了,还是有一堆请求杀到了数据库。
  • 缓存雪崩,一大批缓存同时过期,一大波请求趁虚而入,如同雪崩一般。
  • 下面我们来聊一聊这三个问题如何应对。

数据一致性问题:

     一个系统,如果数据都是不变的,应用 Cache-Aside 模式,可以做到缓存中的数据永远和数据库中一致,需要考虑的就是缓存什么时候过期,或者缓存更新的算法,做到尽可能地找出热点数据即可。 

     但大部分系统是要更新数据的,数据更新了缓存没有及时更新,有时候没有问题,但在一些场景下不能容忍,比如支付宝,你买了东西一看钱没变,于是疯狂买买买,后来突然一下钱全没了,这谁顶的住对不对。

于是我们在写场景下更新缓存,采用先更数据库再更缓存的模式,比如你买了个煎饼果子,支付宝实际余额从 100 变成了 90,你老婆同时在别的地方用你的支付宝又买了杯豆浆,实际余额变成 85,数据库没问题,但你买煎饼果子时缓存服务卡了一下子,更新操作发生在了豆浆事件的后面,你们俩回家一看查出来的余额是 90,以为白嫖了 5 块钱,但其实还是假象。

其实数据一致性问题还是在并发这个范畴内,整体原则就是分析实际场景,尽可能选择既高效又安全的方案。当然这并不是一件容易的事,如果容易就没有那么多年薪百万的架构师了。

缓存穿透:

      引发缓存穿透的情形一般有两种,一是大量查询一个数据库里也没有的数据,这种数据正常不会被缓存,结果每次都要到数据库里兜一圈。那我们可以设置一个规则,数据库没有的数据我们也缓存起来,值设置成空就行了。

另一种情形是,数据库里有这个数据,之前从没人查询过,但突然有那么一瞬间来了一大波请求,缓存根本来不及反应,压力就全都到了数据库上。这种怎么办?两种办法,一是限流,二是预判。

限流好理解,请求少了就反应的过来了。预判怎么预判?你怎么知道哪个数据会被频繁访问?

不好意思,一般还真的知道,一个数据突然被访问的情况,一般是你自己捣鼓出来的什么幺蛾子,比如淘宝要搞双十一,那有些数据一定会被突然频繁访问,这些数据当然能预判个八九不离十。在请求排山倒海般到来之前,先把它填充到缓存里就完事儿了。(这种做法通常称为缓存预热

缓存雪崩:

        其实本质上雪崩和穿透是一类问题,只是出现的阶段不一样,穿透是缓存已经稳定建立起来了,雪崩是缓存突然同时过期了。当然还有一种情况,就是完全还没有缓存的时候,一大波请求涌入。比如缓存没做持久化,结果机房断电了,重启之后就是没有缓存的。

解决方法仍然是限流和缓存预热。其实这些名词也是没意思,奈何总是有人会问,有人会考。

三、缓存应用实战

  3. 1.Redis 与 Spring Data Redis

首先我们要记住,Redis 和 MySQL 一样,是一个数据库管理系统,人家不是就为了做缓存的。

Redis ≠ 缓存 ,只是由于这玩意儿现在访问速度快,但又不能完全替代关系型数据库,所以确实适合用来做关系型数据库的缓存,都是形势所迫,说不定哪一天就翻身了。

     我们要在应用中操纵这个数据库,自然也需要与关系型数据库相似的访问方法。MySQL 我们用 Spring Data JPA,Redis 我们就用 Spring Data Redis。

     其实在此之前,Java 访问 Redis 主要是通过 Jedis 和 Lettuce 两种由不同团队开发的客户端(提供访问、操作所需的 API),Jedis 比较原生,Lettuce 提供的能力更加全面。

Spring Data Redis 是在 Lettuce 的基础上做了一些封装,与 Spring 生态更加贴合,使用起来也更简便。

2.Redis 安装

正常 Redis 只提供 Linux 版本,Windows 版本由微软提供,版本只到 3.2.100,在 2016 年以后就没有再更新过。下载地址为:https://github.com/microsoftarchive/redis/releases

Linux 下可以用 docker 安装镜像,更下方便。我下载的是 Windows 版,但不推荐大家使用。

3. 3.Spring Data Redis 配置

     首先是在 pom.xml 中添加依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis 连接池 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
</dependency>

再在 application.properties 中配置一些参数,常用的有以下几种:

spring.redis.host=localhost
spring.redis.port=6379
# Redis 数据库索引(默认为 0)
spring.redis.database=0
# Redis 服务器连接密码(默认为空)
spring.redis.password=
#连接池最大连接数(使用负值表示没有限制)
spring.redis.lettuce.pool.max-active=8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.lettuce.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=2000
# redis 只用作缓存,不作为 repository
spring.data.redis.repositories.enabled=false

Java 中的对象存储进 Redis 之前需要进行序列化,默认为字节数组。我们为了方便解析,可以将其配置为 JSON 格式。可以创建一个 RedisConfig 类,代码如下:

package com.gm.wj.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    public static final String REDIS_KEY_DATABASE="wj";

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisSerializer<Object> serializer = redisSerializer();
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        // 设置 redisTemplate 的序列化器
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisSerializer<Object> redisSerializer() {
        //创建JSON序列化器
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(objectMapper);
        return serializer;
    }

    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
        //设置Redis缓存有效期为1天
        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
        return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
    }
}

我们一般希望能够更灵活地运用,因此通常选用 RedisTemplate 来实现自由操作。

RedisTemplate 是 Spring Data Redis 提供的一个完成 Redis 操作、异常转换和序列化的类,我们可以类比 JdbcTemplate 去使用它。

4.缓存实现

    下面我们来尝试实现为项目的图书馆页面和笔记本(文章)页面加上缓存。首先编写一个 Service 类,封装我们将要用到的操作。

package com.gm.wj.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;

import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
public class RedisService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 设置带过期时间的缓存
    public void set(String key, Object value, long time) {
        redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
    }

    // 设置缓存
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }
    
    // 根据 key 获得缓存
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
    
    // 根据 key 删除缓存
    public Boolean delete(String key) {
        return redisTemplate.delete(key);
    }

    // 根据 keys 集合批量删除缓存
    public Long delete(Set<String> keys) {
        return redisTemplate.delete(keys);
    }
    
    // 根据正则表达式匹配 keys 获取缓存
    public Set<String> getKeysByPattern(String pattern) {
        return redisTemplate.keys(pattern);
    }
}

注意这里存储对象均被视为 Object,如果存储对象为 String,可以进一步使用 StringRedisTemplate 来实现更贴合字符串的处理方法。

接下来,就可以在具体的 Service 里添加缓存的处理逻辑。

BookService:

     针对获取图书列表的请求,可以先根据设置的 key 查询缓存,如果有则直接从缓存里获取,如果没有则从数据库查询并写入缓存。

public List<Book> list() {
    List<Book> books;
    String key = "booklist";
    Object bookCache = redisService.get(key);

    if (bookCache == null) {
        Sort sort = new Sort(Sort.Direction.DESC, "id");
        books = bookDAO.findAll(sort);
        redisService.set(key, books);
    } else {
        books = CastUtils.objectConvertToList(bookCache, Book.class);
    }
    return books;
}

注意从缓存拿回来的是 Object ,我们需要编写一个方法把它转换为 List:

public static <T> List<T> objectConvertToList(Object obj, Class<T> clazz) {
    List<T> result = new ArrayList<T>();
    if(obj instanceof List<?>)
    {
        for (Object o : (List<?>) obj)
        {
            result.add(clazz.cast(o));
        }
        return result;
    }
    return null;
}

小结

不用记得太多,下面几句话就够了:

缓存是工程思想的产物,是解决不对称问题的一种优秀实践,并得到了广泛应用
缓存的引入会提高项目复杂度,要综合取舍使用方案
Redis 不是缓存,但可以实现缓存服务
在写新的内容之外,我准备背地里偷偷优化一下前面的文章,不过你们都看到这儿了,也没必要回头再去找哪些地方改了,向前看就好了。

很喜欢博主的一句话 :要敢于做困难事,坚持做困难事,困难是人进步的源泉,总有一天你会发现,自己变秃了,也变强了。

总有人觉得一年不如一年,但我始终认为我们就身处在最好的时代,风起云涌,无限可能。

本文转自为 :Vue + Spring Boot 项目实战(二十一):缓存的应用_Evan-Nightly的博客-CSDN博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值