前言

提示:这里可以添加本文要记录的大概内容:

本人目前写的所有文章都是基于springboot项目中可能用到的框架技术,如有需要,可在专栏中寻找。


提示:以下是本篇文章正文内容,下面案例可供参考

一、关于Redis

Redis是一款基于内存使用了类似K-V结构来实现缓存数据的NoSQL非关系型数据库。

提示:Redis本身也会做数据持久化处理。

二、Redis的简单操作

Redis安装链接: https://pan.baidu.com/s/1V9MBzjfXmDyTRdAc8rf7Nw 提取码: 8y3l 当已经安装Redis,并确保环境变量可用后,可以在命令提示符窗口(CMD)或终端(IDEA的Terminal,或MacOS/Linux的命令窗口)中执行相关命令。
另外,推荐安装Redis Desktop Manager软件,可随时查看redis缓存中数据,安装链接: https://pan.baidu.com/s/1b8Nof3K6LGIvwBfqmS_8LQ 提取码: t1y0
在终端下,可以通过redis-cli登录Redis客户端:

redis-cli

在Redis客户端中,可以通过ping检测Redis是否正常工作,将得到PONG的反馈:

ping

在Redis客户端中,可以通过set命令向Redis中存入修改简单类型的数据:

set name jack

在Redis客户端中,可以通过get命令从Redis中取出简单类型的数据:

get name

如果使用的Key并不存在,使用get命令时,得到的结果将是(nil),等效于Java中的null

在Redis客户端中,可以通过keys命令检索Key:

keys *
keys a*

注意:默认情况下,Redis是单线程的,keys命令会执行整个Redis的检索,所以,执行时间可能较长,可能导致阻塞!

三、在Spring Boot项目中读写Redis

首先,需要添加spring-boot-starter-data-redis依赖项:

<!-- Spring Data Redis:读写Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

以上依赖项默认会连接localhost:6379,并且无用户名、无密码,所以,当你的Redis符合此配置,则不需要在application.properties / application.yml中添加任何配置就可以直接编程。如果需要显式的配置,各配置项的属性名分别为:

  • spring.redis.host
  • spring.redis.port
  • spring.redis.username
  • spring.redis.password

在使用以上依赖项实现Redis编程时,需要使用到的工具类型为RedisTemplate,调用此类的对象的方法,即可实现读写Redis中的数据。
在使用之前,应该先在配置类中使用@Bean方法创建RedisTemplate,并实现对RedisTemplate的基础配置,则在项目的根包下创建config.RedisConfiguration类:

package cn.tedu.csmall.product.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;

import java.io.Serializable;

/**
 * Redis的配置类
 *
 * @author java@tedu.cn
 * @version 0.0.1
 */
@Slf4j
@Configuration
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Serializable> redisTemplate(
            RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Serializable> redisTemplate 
                = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setValueSerializer(RedisSerializer.json());
        return redisTemplate;
    }

}

Redis测试

// 使用Redis时,Key是自由命名的
// 建议Key(名称)是分多段的
// 例如“品牌列表”,应该由 brand 和 list 这2个单词组成
// 并且,多个单词之间推荐使用英文的冒号进行分隔,例如:brand:list
// 对于同一种类型的数据,Key的第1段应该是相同的
// 例如,id=6对应的品牌数据的Key应该中:brand:item:6
// keys brand*

@Slf4j
@SpringBootTest
public class RedisTemplateTests {

    @Autowired
    RedisTemplate<String, Serializable> redisTemplate;
    
 @Test
    void testValueOpsSetObject() {
        // ValueOperations:用于实现string(Redis)的读写
        ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
        // 向Redis中“存入” / “修改”数据
        String key = "brand:item:1";
        Brand brand = new Brand();
        brand.setId(1L);
        brand.setName("大白象");
        brand.setEnable(1);
        ops.set(key, brand);
        log.debug("已经向Redis中写入Key={}且Value={}的数据!", key, brand);
      }
      
    @Test
    void testValueOpsGetObject() {
        // ValueOperations:用于实现string(Redis)的读写
        ValueOperations<String, Serializable> ops = redisTemplate.opsForValue();
        // 从Redis中读取数据
        String key = "brand:item:1";
        Serializable value = ops.get(key);
        log.debug("已经从Redis中读取Key={}的数据,Value={}", key, value);
        log.debug("读取到的数据的类型是:{}", value.getClass().getName());
        Brand brand = (Brand) value;
        log.debug("执行类型转换成功:{}", brand);
    }

    @Test
    void testDelete() {
        String key = "brand:item:1";
        Boolean result = redisTemplate.delete(key);
        log.debug("在Redis中删除了Key={}的数据,结果为:{}", key, result);
    }

    @Test
    void testListRightPush() {
    	//向redis中写入列表数据
        List<Brand> brands = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            Brand brand = new Brand();
            brand.setId(i + 0L);
            brand.setName("测试品牌" + i);
            brands.add(brand);
        }

        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        //所有对象共用一个key
        String key = "brand:list";
        for (Brand brand : brands) {
            ops.rightPush(key, brand);
        }
    }

    @Test
    void testListRange() {
        // 取出列表
        // 在列表中的每个元素都有2个下标
        // 正数的下标是从第1个元素以0作为下标,开始递增的编号
        // 负数的下标是从最后一个元素以-1作为下标,开始递减的编号
        ListOperations<String, Serializable> ops = redisTemplate.opsForList();
        String key = "brand:list";
        long start = 0L;
        long end = -1L;
        List<Serializable> list = ops.range(key, start, end);
        log.debug("从Redis中获取到的结果:{}", list);
        for (Serializable serializable : list) {
            log.debug("{}", serializable);
        }
    }

  }

四、在项目中应用Redis

Redis是用于处理“缓存”的,当客户端尝试查询某些数据时,服务器端的处理流程大致是:

  • 优先从Redis中获取数据
    • 如果Redis中没有所需的数据,则从数据库中查询,并将查询结果存入到Redis
  • 将Redis中的数据(或:刚刚从数据库中查询出来的数据)响应到客户端

使用Redis后,可以明显的提高查询效率(当数据表中的数据量大时,效果明显),同时,还能减轻数据库服务器的压力。

在使用之前,还应该确定需要将哪些数据使用Redis处理查询,通常,应该是查询频率可能较高的、允许数据不够准确的(即使数据有一些不准确,但是对整个项目没有严重后果的),甚至这些数据极少改变的。

在具体使用时,可以直接使用RedisTemplate去操作Redis,也可以对RedisTemplate的使用进行再次封装。

在项目中,常见做法是在根包下创建repository包,创建处理数据缓存的仓库接口,这里以品牌数据为例:

/**
 * 处理品牌数据缓存的仓库接口
 */
public interface IBrandRedisRepository {

    /**
     * 品牌数据项的KEY的前缀
     */
    String BRAND_ITEM_PREFIX = "brand:item:";

    /**
     * 品牌列表的KEY
     */
    String BRAND_LIST_KEY = "brand:list";

    /**
     * 向Redis中存入数据
     * @param brand
     */
    void put(BrandStandardVO brand);
    /**
     * 向Redis中存入品牌数据,或替换原有数据,此次存入的数据仅在某段时间内有效的
     *
     * @param brand    品牌数据
     * @param t        存活时间值
     * @param timeUnit 存活时间单位
     */
    void put(BrandStandardVO brand, long t, TimeUnit timeUnit);

    /**
     * 根据id从redis中取出数据
     * @param id
     * @return
     */
    BrandStandardVO get(Long id);

    /**
     * 向Redis中存入品牌列表
     *
     * @param brands 品牌列表
     */
    void putList(List<BrandListItemVO> brands);

    /**
     * 从Redis中读取品牌列表
     *
     * @return 品牌列表,如果Redis中没有品牌列表,则返回长度为0的集合
     */
    List<BrandListItemVO> getList();
     /**
     * 删除缓存中所有keys内容
     * @param keys
     * @return
     */
    Long deleteAll(Collection<String> keys);

    /**
     * 获取所有keys
     * @return
     */
    Set<String> getAllKeys();
}

接着在repository包下创建子包impl,实现接口,实现从redis中读写数据方法。

@Slf4j
@Repository
public class BrandRedisRepositoryImpl implements IBrandRedisRepository {

    @Autowired
    private RedisTemplate<String, Serializable> redisTemplate;

    public BrandRedisRepositoryImpl() {
        log.info("创建了处理缓存的类:BrandRedisRepositoryImpl");
    }

    @Override
    public void put(BrandStandardVO brand) {
        String key = BRAND_ITEM_PREFIX + brand.getId();
        redisTemplate.opsForValue().set(key,brand);
    }

    @Override
    public void put(BrandStandardVO brand, long t, TimeUnit timeUnit) {
        String key = BRAND_ITEM_PREFIX + brand.getId();
        redisTemplate.opsForValue().set(key,brand,t,timeUnit);
    }

    @Override
    public BrandStandardVO get(Long id) {
        String key = BRAND_ITEM_PREFIX + id;
        Serializable serializable = redisTemplate.opsForValue().get(key);
        BrandStandardVO brandStandardVO = null;
        if (serializable != null){
            brandStandardVO=(BrandStandardVO) serializable;
        }
        return brandStandardVO;
    }

    @Override
    public void putList(List<BrandListItemVO> brands) {
        for (BrandListItemVO brand : brands) {
            redisTemplate.opsForList().rightPush(BRAND_LIST_KEY, brand);
        }
    }

    @Override
    public List<BrandListItemVO> getList() {
        List<Serializable> list = redisTemplate.opsForList().range(BRAND_LIST_KEY,0,-1);
        List<BrandListItemVO> brands = new ArrayList<>();
        for (Serializable serializable : list) {
            brands.add((BrandListItemVO) serializable);
        }
        return brands;
    }

    @Override
    public Set<String> getAllKeys() {
        //获取所有brand的keys
        return redisTemplate.keys("brand:*");
    }

    @Override
    public Long deleteAll(Collection<String> keys) {
        return redisTemplate.delete(keys);
    }
}
最后在service实现类中调用,如果redis中不存在,则通过mapper从数据库中查询,再存入redis中,这里以查询详情和查询列表为例。
  @Autowired
    private BrandMapper brandMapper;

    @Autowired
    private IBrandRedisRepository brandRedisRepository;
    
 @Override
    public BrandStandardVO getStandardById(Long id) {
        log.debug("开始处理【查询品牌详情】的业务");
        //根据id从redis中获取数据
        //判断结果是否不为null
        //是:直接返回
        BrandStandardVO brandStandard = brandRedisRepository.get(id);
        if (brandStandard != null){
            log.debug("redis中有此数据,直接返回:{}",brandStandard);
            return brandStandard;
        }

        //redis中无此数据,调用mapper查询
        //判断结果是否为null
        //是:抛出异常
        //否:将查询结果存入redis,并返回此结果
        BrandStandardVO brandStandardVO=brandMapper.getStandardById(id);
        if (brandStandardVO==null){
            String message = "查询品牌详情失败,尝试访问的数据不存在!";
            log.warn(message);
            throw new ServiceException(ServiceCode.ERR_NOT_FOUND, message);
        }
        log.debug("向redis中存入数据:{}",brandStandardVO);
        brandRedisRepository.put(brandStandardVO,1, TimeUnit.MINUTES);
        return brandStandardVO;
    }

    @Override
    public List<BrandListItemVO> list() {
        log.debug("开始处理【查询品牌列表】的业务");
        //从redis中读取品牌列表
        List<BrandListItemVO> brands = brandRedisRepository.getList();
        //如果读取到有效列表,表示redis中存在
        if (brands.size() > 0){
            // 直接返回
            return brands;
        }

        // 如果读取到的结果为空列表,表示Redis中无此数据
        // 调用Mapper从数据库中查询,并存入到Redis,并返回
        List<BrandListItemVO> list=brandMapper.list();
        brandRedisRepository.putList(list);
        return list;
    }

五、关于缓存预热

缓存预热:启动项目时,就将缓存数据加载到Redis中。

在Spring Boot项目中,当需要实现“启动项目时直接执行”的效果,需要自定义组件类,实现ApplicationRunner接口,重写其中的run()方法,此run()将在项目启动成功后自动执行

提示:缓存预热的操作应该通过ApplicationRunner来实现,这样才可以保证在所有组件都已经正确的创建后再执行缓存预热,如果通过某些组件的构造方法来编写缓存预热的代码,此时某些组件可能还没有创建,则无法正确执行。

关于缓存预热的具体实现:

  • 删除所有相关的缓存数据
    • 删除列表数据:如果不删除,再次向缓存中写入列表,将是在原列表的基础上追加,则会产生重复的列表项
    • 删除数据项(每一个数据):如果不删除,则会导致原本已经缓存的数据一直存在,某些数据可能在数据库中已经删除,则缓存中的数据也应该被删除
  • 从数据库中查询列表,并写入到缓存
  • 基于查询到的列表,遍历,得到每个数据的id,再从数据库中查出各数据,并写入到缓存

在项目根包下,创建preload包,创建CacheLoader类实现ApplicationRunner接口:

@Component
@Slf4j
public class CacheLoader implements ApplicationRunner {

    @Autowired
    private IBrandService brandService;

    public CacheLoader() {
        log.debug("创建缓存加载器ApplicationRunner:CacheLoader");
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.debug("CacheLoader.run()");
        log.debug("预加载品牌数据到缓存");
        brandService.loadBrandToCache();
    }
}

在service实现类中:

    @Override
    public void loadBrandToCache() {
        // 删除原有的缓存的品牌数据
        Set<String> keys = brandRedisRepository.getAllKeys();
        brandRedisRepository.deleteAll(keys);

        // 调用Mapper查询品牌列表
        List<BrandListItemVO> list = brandMapper.list();
        // 调用RedisRepository将品牌列表写入到缓存中
        brandRedisRepository.putList(list);

        // 以上Mapper查询结果包含所有品牌的数据,每个数据中都有id
        // 遍历Mapper查询结果,并调用Mapper根据id查询每个品牌数据
        // 调用RedisRepository将查询到的数据写入到缓存中
        for (BrandListItemVO brand : list) {
            BrandStandardVO brandStandardVO = brandMapper.getStandardById(brand.getId());
            brandRedisRepository.put(brandStandardVO);
        }
    }

六、关于自动更新缓存

更新缓存的策略有多种,通常使用的可以是:

  • 手动更新
    • 适用于数据变化频率非常低的应用场景,这些数据的缓存可以是长期存在,偶尔需要更新时,手动更新即可
  • 自动更新
    • 适用于数据频繁的变化,通过手动更新不太现实,将会是每间隔一段时间,或在特定的某个时间(例如每周一凌晨3点)自动更新

关于自动更新,需要使用到“计划任务”。

使用计划任务,需要自定义组件类,然后,在类中自定义方法(应该是public权限,返回值类型声明为void,参数列表为空),这个方法将作为计划任务执行的方法,在此方法上需要添加@Scheduled注解,并配置其执行频率或特定的执行时间,最后,还需要在配置类上使用@EnableScheduling注解,以开启当前项目的计划任务。
在项目根包下创建schedule包,并创建CacheSchedule类:

@Slf4j
@Component
public class CacheSchedule {

    @Autowired
    private IBrandService brandService;

    public CacheSchedule() {
        log.debug("创建计划任务对象:CacheSchedule");
    }

    // 关于@Scheduled常用属性
    // >> fixedRate:每间隔多少毫秒执行一次
    // >> fixedDelay:每延迟多少毫秒执行一次
    // >> cron:使用1个字符串,其中写6-7个值,各值之间使用空格进行分隔
    // >> >> 这6-7个值分别表示:秒 分 时 日 月 周 [年]
    // >> >> 例如:cron = "56 34 12 20 1 ? 2230",表示“2230年1月20日12:34:56执行,无论当天是星期几”
    // >> >> 以上各个位置,均可以使用星号,表示任意值,在“日”和“周”上,还可以使用问号,表示不关心具体值
    // >> >> 以上各个位置,还可以使用"x/x"格式的值,例如,在"分钟"位置使用 1/5,表示分钟值为1时执行,且每5分钟执行1次
    @Scheduled(fixedRate = 3 * 60 * 1000)
    public void updateCache() {
        log.debug("执行了CacheSchedule的计划任务……");
        log.debug("加载品牌数据到缓存……");
        brandService.loadBrandToCache();
    }
}

然后创建配置类,配置类上使用@EnableScheduling注解,以开启当前项目计划任务。以上计划内容指每隔3分钟更新缓存一次。

@Configuration
@EnableScheduling
public class ScheduleConfiguration {
}

总结

提示:这里对文章进行总结:

例如:以上就是今天要讲的内容,从简单介绍redis的基本操刀到在项目中redis的应用,再到缓存预热的介绍,最后介绍了创建更新缓存计划内容,由浅入深逐步剖析。欢迎点赞评论交流。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐