美文网首页
还在为缓存头疼?来看看更优雅的缓存方案(一)

还在为缓存头疼?来看看更优雅的缓存方案(一)

作者: Felix_ | 来源:发表于2021-06-10 18:22 被阅读0次

项目中我们会经常使用Redis来缓存一些信息,已减小数据库的压力,增大并发量.平时缓存信息的时候,会经常写这样的代码

以下为伪代码

if (cache != null){
  return cache;
}else{
  cache = getDataFromDB();
  putCacheToRedis(cache);
}

这样就可以简单的为我们缓存数据,如果命中了缓存,那么直接从缓存中获取数据,否则就去数据库中查询数据,然后放入缓存中.我们先不说这种写法存在缓存穿透缓存雪崩缓存击穿的风险,单单在各个服务中写相同的代码就已经花费了不少时间.为了减少大量的写重复代码,就可以考虑使用SpringCache为我们做以上的工作,当然,也可以通过简单的配置和修改为我们解决缓存穿透缓存雪崩缓存击穿这些问题.

这里我们使用Springboot来集成SpringCache

  • 首先Maven中引入依赖包(这里引入了Jedis连接池)
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
  • 然后添加配置
    在配置文件中,声明要使用缓存的类型为type
spring:
  application:
    name: test1
  cache:
    type: redis
    redis:
      time-to-live: 3600000  #单位mm
      cache-null-values: true  #是否缓存空值
  redis:
    host: 127.0.0.1 # Redis服务器地址
    database: 0 # Redis数据库索引(默认为0)
    port: 6379 # Redis服务器连接端口
    password: yourpassword # Redis服务器连接密码(默认为空)
    jedis:
      pool:
        max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
        max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-idle: 8 # 连接池中的最大空闲连接
        min-idle: 0 # 连接池中的最小空闲连接
    connect-timeout: 2000
    timeout: 2000
server:
  port: 8000

接下来,我们编写一个简单的服务,来使用SpringCache,这里我搭建了一个redisTest1的模块

image.png
我粗略的展示一下各个类

Redistest1Application

package com.felix.redistest;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
public class Redistest1Application {

    public static void main(String[] args) {
        SpringApplication.run(Redistest1Application.class, args);
    }

}

Test1Service

package com.felix.redistest.service;

import model.TestProduct;

import java.util.List;

public interface Test1Service {
    List<TestProduct> updateProducts(Integer count);
    List<TestProduct> getAllProducts();
    String buyProduct();
}

CacheService

package com.felix.redistest.service;

import model.TestProduct;

import java.util.List;

public interface CacheService {
    List<TestProduct> updateProducts(Integer count);
    List<TestProduct> getAllProducts();
    List<TestProduct> reduceProduct(List<TestProduct> products);
}

Test1ServiceImpl

package com.felix.redistest.service.impl;

import com.felix.redistest.service.CacheService;
import com.felix.redistest.service.Test1Service;
import model.TestProduct;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

@Service
public class Test1ServiceImpl implements Test1Service {
    @Resource
    private CacheService cacheService;
    @Resource
    private RedissonClient redissonClient;

    @Override
    public List<TestProduct> updateProducts(Integer count) {
        return cacheService.updateProducts(count);
    }

    @Override
    public List<TestProduct> getAllProducts() {
        List<TestProduct> products = cacheService.getAllProducts();
        System.out.println("获取到商品信息" + new Date().toString());
        return products;
    }

    @Override
    public String buyProduct() {
        System.out.println(Thread.currentThread().getId());
        List<TestProduct> products = getAllProducts();
        if (!CollectionUtils.isEmpty(products)){
            System.out.println("开始购买商品");
            cacheService.reduceProduct(products);
        }else{
            System.out.println("异常:商品不存在");
        }
        return "抢购成功";
    }
}

CacheServiceImpl

package com.felix.redistest.service.impl;

import com.felix.redistest.service.CacheService;
import model.TestProduct;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Random;

@Service
public class CacheServiceImpl implements CacheService {
    @CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
    @Override
    public List<TestProduct> updateProducts(Integer count) {
        System.out.println("更新商品缓存信息");
        return getProductsFromDB(count);
    }


    @Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'",sync = true)
    @Override
    public List<TestProduct> getAllProducts() {
        System.out.println("查询缓存信息" + new Date().toString());
        return getProductsFromDB(3);
    }


    @CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
    @Override
    public List<TestProduct> reduceProduct(List<TestProduct> products) {
        System.out.println("扣减库存,刷新商品信息");
        TestProduct product = products.get(0);
        products.remove(0);
        product.setStock(product.getStock() - 1);
        products.add(0,product);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return products;
    }
    List<TestProduct> getProductsFromDB(Integer count){
        List<TestProduct> products = new ArrayList<>();
        Random random = new Random();
        for (int i = 0;i < count;i++){
            TestProduct product = new TestProduct();
            product.setId(Long.parseLong(i + 1 + ""));
            product.setName("product" + "-i");
            product.setStock(random.nextInt(1000) + 10);
            products.add(product);
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return products;
    }
}

Test1Controller

package com.felix.redistest.controller;

import com.felix.redistest.service.Test1Service;
import model.TestProduct;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping("/test")
public class Test1Controller {
    @Resource
    private Test1Service test1Service;


    @GetMapping(value = "/updateCache")
    public List<TestProduct> updateCache(Integer count){
        return test1Service.updateProducts(count);
    }

    @GetMapping(value = "/getCache")
    public List<TestProduct> getCache(){
        return test1Service.getAllProducts();
    }

    @GetMapping(value = "/buy")
    public Object buy(){
        String result = test1Service.buyProduct();
        return result;
    }
}

WSTKRedisConfig

package com.felix.redistest.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
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.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching
@Configuration
@EnableConfigurationProperties({CacheProperties.class})
public class WSTKRedisConfig extends CachingConfigurerSupport {
    @Autowired
    private CacheProperties cacheProperties;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    
    @Bean
    public CacheManager cacheManager() {
        RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
        configuration = configuration.serializeKeysWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                        new StringRedisSerializer()
                )
        );
        configuration = configuration.serializeValuesWith(
                RedisSerializationContext.SerializationPair.fromSerializer(
                        new GenericJackson2JsonRedisSerializer()
                )
        );
        if (cacheProperties.getRedis().getTimeToLive() != null) {
            configuration = configuration.entryTtl(cacheProperties.getRedis().getTimeToLive());
        }
        if (cacheProperties.getRedis().getKeyPrefix() != null) {
            configuration = configuration.prefixCacheNameWith(cacheProperties.getRedis().getKeyPrefix());
        }
        if (!cacheProperties.getRedis().isCacheNullValues()) {
            configuration = configuration.disableCachingNullValues();
        }
        if (!cacheProperties.getRedis().isUseKeyPrefix()) {
            configuration = configuration.disableKeyPrefix();
        }
        WSTKRedisCacheManager redisCacheManager = new WSTKRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory), configuration);
        return redisCacheManager;
    }
}

WSTKRedisCacheManager

package com.felix.redistest.config;

import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.util.StringUtils;

import java.time.Duration;

public class WSTKRedisCacheManager extends RedisCacheManager {

    public WSTKRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
        super(cacheWriter, defaultCacheConfiguration);
    }

    @Override
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
        String[] array = StringUtils.delimitedListToStringArray(name, "#");
        name = array[0];
        if (array.length > 1) {
            long ttl = Long.parseLong(array[1]);
            //单位为秒
            cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(ttl));
        }
        return super.createRedisCache(name, cacheConfig);
    }
}

逻辑比较简单,分别提供了更新缓存获取缓存购买商品三个接口,我们简单分析一下它们的实现


  • WSTKRedisConfig配置了SpringCache保存到redis中使用的序列化工具,如果不自定义,那么会使用JAVA的序列化工具,在redis中看到的数据只能无法用于其他编程语言解析

  • WSTKRedisConfig中注入了自定义的WSTKRedisCacheManager,这里我们主要是在创建redis缓存的时候,可以自定义同一类key的过期时间,使用#隔开

  • CacheServiceImpl 中的updateProducts方法上,使用了@CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")注解,代表我们将会缓存updateProducts的返回值为value,keyPMS_PRODUCT::PRODUCT,过期时间为120s,这里的#120之所以生效,就是使用了我们自定义的WSTKRedisCacheManager

  • CacheServiceImpl 中的getAllProducts方法上,使用了@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")注解,代表我们将会缓存updateProducts的返回值为value,keyPMS_PRODUCT::PRODUCT,过期时间为120s,那么为什么不使用@CachePut呢?其实从我定义的方法名上就可以看出来了.解释一下,@CachePut不管缓存中有没有值,都会执行我们的操作,然后存入/覆盖redis,但是@Cacheable在命中缓存的话只读取不写入,没有命中缓存才会写入缓存

  • CacheServiceImpl 中的reduceProduct方法上,使用了@CachePut(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")注解,代表我们将会缓存updateProducts的返回值为value,keyPMS_PRODUCT::PRODUCT,过期时间为120s.这里其实跟业务有关,当我们操作了缓存,肯定希望重新更新缓存中的值,所以使用了@CachePut,当然,我们可以使用组合注解达成目的,比如这里就可以写成

@Caching(
            evict = {@CacheEvict(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")},
            cacheable = {@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")}
    )

这种组合形式,代表我们将会先删除PMS_PRODUCT::PRODUCT,然后再缓存新的商品信息到PMS_PRODUCT::PRODUCT

我们分别来尝试一下,首先访问localhost:8000/test/updateCache?count=3生成三条商品数据,生成会花费10s的时间

image.png
当然,如果我们第二次访问该接口,虽然有缓存,但是仍然会重新生成商品数据,并且延迟10s才返回数据,刷新了缓存

然后访问[localhost:8000/test/getCache](localhost:8000/test/getCache),如果没有命中缓存,那么就会重新花费10s并存入缓存,否则会直接返回缓存中的数据

image.png
此次请求很快就返回了商品数据信息,可见,第一步的updateCache已经生成了缓存并,多次访问localhost:8000/test/getCache得到的数据和之前的一样,代表读取的其实是缓存的数据

再来redis中验证一下


image.png

可见数据已经成功缓存了下来

有些朋友可能会注意到,为什么要单独独立出来一个CacheService呢?不仅仅是为了逻辑更清晰,spring cache的实现原理跟spring的事物管理类似,都是基于AOP的动态代理实现的:即都在方法调用前后 去获取方法的名称、参数、返回值,然后根据方法名称、参数生成缓存的key(自定义的key例外),进行缓存,所以,假如放置updateProducts、getAllProducts这些方法到内部的话,实际上是无法使用缓存的

到此,虽然使用SpringCache让开发高效了,但是好像也并没有提到解决缓存穿透缓存雪崩缓存击穿的问题啊.当然不止于此,我会在下节解释并实际操作,来依次解决这些问题.

以上内容转载请注明出处,同时也请大家不吝你的关注和下面的赞赏
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

相关文章

网友评论

      本文标题:还在为缓存头疼?来看看更优雅的缓存方案(一)

      本文链接:https://www.haomeiwen.com/subject/jekzsltx.html