redis整合SpringBoot实现数据缓存

一,使用缓存的必要性

当服务器端收到客户端请求量变多时,某些数据请求量大也会随之变大,这些热点数据要频繁的从数据库中读取,给数据库造成压力,自然会导致服务器响应客户端变慢。因此,在一些不考虑实时性的数据中,我们通常会将这些数据临时存储存在内存中,当请求时候,我们就能够直接读取内存中的数据及时响应。这就是使用缓存的初衷。

缓存主要用于解决高性能与高并发以时减少数据库压力的作用。它的本质就是将数据存储在内存中,当数据没有发生本质变化的时候,应尽量避免直接连接数据库进行查询,因为并发高时很可能会将数据库压塌,而是应去缓存中读取数据,只有缓存中未查找到时再去数据库中查询,这样就大大降低了数据库的读写次数,增加系统的性能和能提供的并发量。

二,redis作为缓存的优势

  • Redis提供了高性能的数据存储功能。单个Redis server对请求的处理是基于单线程工作模型的,但由于是纯内存操作,并且单线程的工作模式避免了线程上下文切换带来的额外开销,同时使用NIO多路复用机制(单线程维护多个I/O socket的状态,socket event handler统一进行event分发,并通知到各个event listener),所以即使是单台Redis server的性能也是非常的快,可支持11万次/秒的SET操作,8.1万次/秒的GET操作。这样可以减少网络的IO次数和数据体积。

  • Redis将其数据完全保存在内存中,仅使用磁盘进行持久化。与其它键值数据存储相比,Redis有一组相对丰富的数据类型。支持string,list,set,sorted set,hash

  • Redis支持数据持久化和数据恢复,支持master/slave主/从机制,sentinal哨兵模式以及cluster集群模式,允许单点故障,虽然同时也会付出性能的代价,但是相比于MemCached,其稳定性还是有保证的(MemCached不支持数据持久化,断电或重启后数据消失)

  • Redis支持事务,操作具有原子性 - 所有Redis操作都是原子操作,这确保如果两个客户端并发访问,Redis服务器能接收更新的值。

三,redis作为缓存的一般逻辑图

如下:

采用redis作为Mysql数据库的缓存,在查找的时候,首先查找redis缓存,如果找到则返回结果;如果在redis中没有找到,那么查找Mysql数据库,找到的花则返回结果并且更新redis;如果没有找到则返回空。对于写入的情况,直接写入mysql数据库,mysql数据库通过触发器及UDF机制自动把变更的内容更新到redis中。

四,redis整合SpringBoot使用的两种方式

4.1,纯代码手工实现

由于这种方法笔者比较复杂,代码量也多,因此并不推荐,只做简单实现,供大家了解一下即可。

4.1.1,创建maven项目并导入相关的mysql,redis依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.yy</groupId>
    <artifactId>springboot-08-jedis</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-08-jedis</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

4.1.2,配置yml文件

spring:
  datasource:
    username: root
    password: 123456
    #时区报错,需要添加时区
    url: jdbc:mysql://localhost:3306/school?serverTimezone=UTC&useSSL=true&useUnicode=true&characterEncoding=UTF-8
    driver-class-name: com.mysql.cj.jdbc.Driver
---
#springboot-redis配置
spring:
  redis:
    host: redis地址
    port: 6379
    password: 123456
    database: 0

4.1.3,配置RedisTemplate模板

从依赖项中我们可以看到,SpringBoot集成redis的依赖中为我们提供了redisTamplate类来对redis进行操作。

其中他为我们提供了默认的序列化方式 :JdkSerializationRedisSerializer,作为JDK提供的序列化功能。 这个序列化方式优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。并且这种默认序列化方式在RDM工具中查看k-v值时会出现“乱码”,不方便查看。

因此我们需要自己去自定义序列化方式来更好的方便我们操作。在此,我更推荐去配置Jackson2JsonRedisSerializer序列化方式,因为它是使用Jackson库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。

在redis配置类中配置自定义的序列化方式

package com.yy.myconfig;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * 自定义一个redis template模板
 */
@Configuration
public class RedisConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        //为了使开发方便,直接使用<String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        //Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //string的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用string的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也是用string的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        //value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;
    }
}

 为了方便我们对reids不同的数据类型的操作,有需要的同学也可以将redisTamplate封装为一个工具类,以便后面直接调用。

package com.yy.utils;

import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.sql.Date;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtils {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public RedisTemplate<String, Object> getRedisTemplate() {
        return this.redisTemplate;
    }

    /** -------------------key相关操作--------------------- */

    /**
     * 删除key
     *
     * @param key
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 批量删除key
     *
     * @param keys
     */
    public void delete(Collection<String> keys) {
        redisTemplate.delete(keys);
    }

    /**
     * 序列化key
     *
     * @param key
     * @return
     */
    public byte[] dump(String key) {
        return redisTemplate.dump(key);
    }

    /**
     * 是否存在key
     *
     * @param key
     * @return
     */
    public Boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 设置过期时间
     *
     * @param key
     * @param timeout
     * @param unit
     * @return
     */
    public Boolean expire(String key, long timeout, TimeUnit unit) {
        return redisTemplate.expire(key, timeout, unit);
    }

    /**
     * 设置过期时间
     *
     * @param key
     * @param date
     * @return
     */
    public Boolean expireAt(String key, Date date) {
        return redisTemplate.expireAt(key, date);
    }

    /**
     * 查找匹配的key
     *
     * @param pattern
     * @return
     */
    public Set<String> keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 将当前数据库的 key 移动到给定的数据库 db 当中
     *
     * @param key
     * @param dbIndex
     * @return
     */
    public Boolean move(String key, int dbIndex) {
        return redisTemplate.move(key, dbIndex);
    }

    /**
     * 移除 key 的过期时间,key 将持久保持
     *
     * @param key
     * @return
     */
    public Boolean persist(String key) {
        return redisTemplate.persist(key);
    }

    /**
     * 返回 key 的剩余的过期时间
     *
     * @param key
     * @param unit
     * @return
     */
    public Long getExpire(String key, TimeUnit unit) {
        return redisTemplate.getExpire(key, unit);
    }

    /**
     * 返回 key 的剩余的过期时间
     *
     * @param key
     * @return
     */
    public Long getExpire(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 从当前数据库中随机返回一个 key
     *
     * @return
     */
    public String randomKey() {
        return (String) redisTemplate.randomKey();
    }

    /**
     * 修改 key 的名称
     *
     * @param oldKey
     * @param newKey
     */
    public void rename(String oldKey, String newKey) {
        redisTemplate.rename(oldKey, newKey);
    }

    /**
     * 仅当 newkey 不存在时,将 oldKey 改名为 newkey
     *
     * @param oldKey
     * @param newKey
     * @return
     */
    public Boolean renameIfAbsent(String oldKey, String newKey) {
        return redisTemplate.renameIfAbsent(oldKey, newKey);
    }

    /**
     * 返回 key 所储存的值的类型
     *
     * @param key
     * @return
     */
    public DataType type(String key) {
        return redisTemplate.type(key);
    }

    /** -------------------string相关操作--------------------- */

    /**
     * 设置指定 key 的值
     *
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 获取指定 key 的值
     *
     * @param key
     * @return
     */
    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    /**
     * 返回 key 中字符串值的子字符
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public String getRange(String key, long start, long end) {
        return redisTemplate.opsForValue().get(key, start, end);
    }

    /**
     * 将给定 key 的值设为 value ,并返回 key 的旧值(old value)
     *
     * @param key
     * @param value
     * @return
     */
    public String getAndSet(String key, String value) {
        return (String) redisTemplate.opsForValue().getAndSet(key, value);
    }

    /**
     * 对 key 所储存的字符串值,获取指定偏移量上的位(bit)
     *
     * @param key
     * @param offset
     * @return
     */
    public Boolean getBit(String key, long offset) {
        return redisTemplate.opsForValue().getBit(key, offset);
    }

    /**
     * 批量获取
     *
     * @param keys
     * @return
     */
    public List<Object> multiGet(Collection<String> keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
     * 设置ASCII码, 字符串'a'的ASCII码是97, 转为二进制是'01100001', 此方法是将二进制第offset位值变为value
     *
     * @param key   位置
     * @param value 值,true为1, false为0
     * @return
     */
    public boolean setBit(String key, long offset, boolean value) {
        return redisTemplate.opsForValue().setBit(key, offset, value);
    }

    /**
     * 将值 value 关联到 key ,并将 key 的过期时间设为 timeout
     *
     * @param key
     * @param value
     * @param timeout 过期时间
     * @param unit    时间单位, 天:TimeUnit.DAYS 小时:TimeUnit.HOURS 分钟:TimeUnit.MINUTES
     *                秒:TimeUnit.SECONDS 毫秒:TimeUnit.MILLISECONDS
     */
    public void setEx(String key, String value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
     * 只有在 key 不存在时设置 key 的值
     *
     * @param key
     * @param value
     * @return 之前已经存在返回false, 不存在返回true
     */
    public boolean setIfAbsent(String key, String value) {
        return redisTemplate.opsForValue().setIfAbsent(key, value);
    }

    /**
     * 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始
     *
     * @param key
     * @param value
     * @param offset 从指定位置开始覆写
     */
    public void setRange(String key, String value, long offset) {
        redisTemplate.opsForValue().set(key, value, offset);
    }

    /**
     * 获取字符串的长度
     *
     * @param key
     * @return
     */
    public Long size(String key) {
        return redisTemplate.opsForValue().size(key);
    }

    /**
     * 批量添加
     *
     * @param maps
     */
    public void multiSet(Map<String, String> maps) {
        redisTemplate.opsForValue().multiSet(maps);
    }

    /**
     * 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在
     *
     * @param maps
     * @return 之前已经存在返回false, 不存在返回true
     */
    public boolean multiSetIfAbsent(Map<String, String> maps) {
        return redisTemplate.opsForValue().multiSetIfAbsent(maps);
    }

    /**
     * 增加(自增长), 负数则为自减
     *
     * @param key
     * @return
     */
    public Long incrBy(String key, long increment) {
        return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
     * @param key
     * @return
     */
    public Double incrByFloat(String key, double increment) {
        return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
     * 追加到末尾
     *
     * @param key
     * @param value
     * @return
     */
    public Integer append(String key, String value) {
        return redisTemplate.opsForValue().append(key, value);
    }

    /** -------------------hash相关操作------------------------- */

    /**
     * 获取存储在哈希表中指定字段的值
     *
     * @param key
     * @param field
     * @return
     */
    public Object hGet(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 获取所有给定字段的值
     *
     * @param key
     * @return
     */
    public Map<Object, Object> hGetAll(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * 获取所有给定字段的值
     *
     * @param key
     * @param fields
     * @return
     */
    public List<Object> hMultiGet(String key, Collection<Object> fields) {
        return redisTemplate.opsForHash().multiGet(key, fields);
    }

    public void hPut(String key, String hashKey, String value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
    }

    public void hPutAll(String key, Map<String, String> maps) {
        redisTemplate.opsForHash().putAll(key, maps);
    }

    /**
     * 仅当hashKey不存在时才设置
     *
     * @param key
     * @param hashKey
     * @param value
     * @return
     */
    public Boolean hPutIfAbsent(String key, String hashKey, String value) {
        return redisTemplate.opsForHash().putIfAbsent(key, hashKey, value);
    }

    /**
     * 删除一个或多个哈希表字段
     *
     * @param key
     * @param fields
     * @return
     */
    public Long hDelete(String key, Object... fields) {
        return redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 查看哈希表 key 中,指定的字段是否存在
     *
     * @param key
     * @param field
     * @return
     */
    public boolean hExists(String key, String field) {
        return redisTemplate.opsForHash().hasKey(key, field);
    }

    /**
     * 为哈希表 key 中的指定字段的整数值加上增量 increment
     *
     * @param key
     * @param field
     * @param increment
     * @return
     */
    public Long hIncrBy(String key, Object field, long increment) {
        return redisTemplate.opsForHash().increment(key, field, increment);
    }

    /**
     * 为哈希表 key 中的指定字段的整数值加上增量 increment
     *
     * @param key
     * @param field
     * @param delta
     * @return
     */
    public Double hIncrByFloat(String key, Object field, double delta) {
        return redisTemplate.opsForHash().increment(key, field, delta);
    }

    /**
     * 获取所有哈希表中的字段
     *
     * @param key
     * @return
     */
    public Set<Object> hKeys(String key) {
        return redisTemplate.opsForHash().keys(key);
    }

    /**
     * 获取哈希表中字段的数量
     *
     * @param key
     * @return
     */
    public Long hSize(String key) {
        return redisTemplate.opsForHash().size(key);
    }

    /**
     * 获取哈希表中所有值
     *
     * @param key
     * @return
     */
    public List<Object> hValues(String key) {
        return redisTemplate.opsForHash().values(key);
    }

    /**
     * 迭代哈希表中的键值对
     *
     * @param key
     * @param options
     * @return
     */
    public Cursor<Map.Entry<Object, Object>> hScan(String key, ScanOptions options) {
        return redisTemplate.opsForHash().scan(key, options);
    }

    /** ------------------------list相关操作---------------------------- */

    /**
     * 通过索引获取列表中的元素
     *
     * @param key
     * @param index
     * @return
     */
    public String lIndex(String key, long index) {
        return (String) redisTemplate.opsForList().index(key, index);
    }

    /**
     * 获取列表指定范围内的元素
     *
     * @param key
     * @param start 开始位置, 0是开始位置
     * @param end   结束位置, -1返回所有
     * @return
     */
    public List<Object> lRange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    /**
     * 存储在list头部
     *
     * @param key
     * @param value
     * @return
     */
    public Long lLeftPush(String key, String value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * @param key
     * @param value
     * @return
     */
    public Long lLeftPushAll(String key, String... value) {
        return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /**
     * @param key
     * @param value
     * @return
     */
    public Long lLeftPushAll(String key, Collection<String> value) {
        return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /**
     * 当list存在的时候才加入
     *
     * @param key
     * @param value
     * @return
     */
    public Long lLeftPushIfPresent(String key, String value) {
        return redisTemplate.opsForList().leftPushIfPresent(key, value);
    }

    /**
     * 如果pivot存在,再pivot前面添加
     *
     * @param key
     * @param pivot
     * @param value
     * @return
     */
    public Long lLeftPush(String key, String pivot, String value) {
        return redisTemplate.opsForList().leftPush(key, pivot, value);
    }

    /**
     * @param key
     * @param value
     * @return
     */
    public Long lRightPush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    /**
     * @param key
     * @param value
     * @return
     */
    public Long lRightPushAll(String key, String... value) {
        return redisTemplate.opsForList().rightPushAll(key, value);
    }

    /**
     * @param key
     * @param value
     * @return
     */
    public Long lRightPushAll(String key, Collection<String> value) {
        return redisTemplate.opsForList().rightPushAll(key, value);
    }

    /**
     * 为已存在的列表添加值
     *
     * @param key
     * @param value
     * @return
     */
    public Long lRightPushIfPresent(String key, String value) {
        return redisTemplate.opsForList().rightPushIfPresent(key, value);
    }

    /**
     * 在pivot元素的右边添加值
     *
     * @param key
     * @param pivot
     * @param value
     * @return
     */
    public Long lRightPush(String key, String pivot, String value) {
        return redisTemplate.opsForList().rightPush(key, pivot, value);
    }

    /**
     * 通过索引设置列表元素的值
     *
     * @param key
     * @param index 位置
     * @param value
     */
    public void lSet(String key, long index, String value) {
        redisTemplate.opsForList().set(key, index, value);
    }

    /**
     * 移出并获取列表的第一个元素
     *
     * @param key
     * @return 删除的元素
     */
    public String lLeftPop(String key) {
        return (String) redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
     *
     * @param key
     * @param timeout 等待时间
     * @param unit    时间单位
     * @return
     */
    public String lBLeftPop(String key, long timeout, TimeUnit unit) {
        return (String) redisTemplate.opsForList().leftPop(key, timeout, unit);
    }

    /**
     * 移除并获取列表最后一个元素
     *
     * @param key
     * @return 删除的元素
     */
    public String lRightPop(String key) {
        return (String) redisTemplate.opsForList().rightPop(key);
    }

    /**
     * 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
     *
     * @param key
     * @param timeout 等待时间
     * @param unit    时间单位
     * @return
     */
    public String lBRightPop(String key, long timeout, TimeUnit unit) {
        return (String) redisTemplate.opsForList().rightPop(key, timeout, unit);
    }

    /**
     * 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
     *
     * @param sourceKey
     * @param destinationKey
     * @return
     */
    public String lRightPopAndLeftPush(String sourceKey, String destinationKey) {
        return (String) redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
                destinationKey);
    }

    /**
     * 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
     *
     * @param sourceKey
     * @param destinationKey
     * @param timeout
     * @param unit
     * @return
     */
    public String lBRightPopAndLeftPush(String sourceKey, String destinationKey,
                                        long timeout, TimeUnit unit) {
        return (String) redisTemplate.opsForList().rightPopAndLeftPush(sourceKey,
                destinationKey, timeout, unit);
    }

    /**
     * 删除集合中值等于value得元素
     *
     * @param key
     * @param index index=0, 删除所有值等于value的元素; index>0, 从头部开始删除第一个值等于value的元素;
     *              index<0, 从尾部开始删除第一个值等于value的元素;
     * @param value
     * @return
     */
    public Long lRemove(String key, long index, String value) {
        return redisTemplate.opsForList().remove(key, index, value);
    }

    /**
     * 裁剪list
     *
     * @param key
     * @param start
     * @param end
     */
    public void lTrim(String key, long start, long end) {
        redisTemplate.opsForList().trim(key, start, end);
    }

    /**
     * 获取列表长度
     *
     * @param key
     * @return
     */
    public Long lLen(String key) {
        return redisTemplate.opsForList().size(key);
    }

    /** --------------------set相关操作-------------------------- */

    /**
     * set添加元素
     *
     * @param key
     * @param values
     * @return
     */
    public Long sAdd(String key, String... values) {
        return redisTemplate.opsForSet().add(key, values);
    }

    /**
     * set移除元素
     *
     * @param key
     * @param values
     * @return
     */
    public Long sRemove(String key, Object... values) {
        return redisTemplate.opsForSet().remove(key, values);
    }

    /**
     * 移除并返回集合的一个随机元素
     *
     * @param key
     * @return
     */
    public String sPop(String key) {
        return (String) redisTemplate.opsForSet().pop(key);
    }

    /**
     * 将元素value从一个集合移到另一个集合
     *
     * @param key
     * @param value
     * @param destKey
     * @return
     */
    public Boolean sMove(String key, String value, String destKey) {
        return redisTemplate.opsForSet().move(key, value, destKey);
    }

    /**
     * 获取集合的大小
     *
     * @param key
     * @return
     */
    public Long sSize(String key) {
        return redisTemplate.opsForSet().size(key);
    }

    /**
     * 判断集合是否包含value
     *
     * @param key
     * @param value
     * @return
     */
    public Boolean sIsMember(String key, Object value) {
        return redisTemplate.opsForSet().isMember(key, value);
    }

    /**
     * 获取两个集合的交集
     *
     * @param key
     * @param otherKey
     * @return
     */
    public Set<Object> sIntersect(String key, String otherKey) {
        return redisTemplate.opsForSet().intersect(key, otherKey);
    }

    /**
     * 获取key集合与多个集合的交集
     *
     * @param key
     * @param otherKeys
     * @return
     */
    public Set<Object> sIntersect(String key, Collection<String> otherKeys) {
        return redisTemplate.opsForSet().intersect(key, otherKeys);
    }

    /**
     * key集合与otherKey集合的交集存储到destKey集合中
     *
     * @param key
     * @param otherKey
     * @param destKey
     * @return
     */
    public Long sIntersectAndStore(String key, String otherKey, String destKey) {
        return redisTemplate.opsForSet().intersectAndStore(key, otherKey,
                destKey);
    }

    /**
     * key集合与多个集合的交集存储到destKey集合中
     *
     * @param key
     * @param otherKeys
     * @param destKey
     * @return
     */
    public Long sIntersectAndStore(String key, Collection<String> otherKeys,
                                   String destKey) {
        return redisTemplate.opsForSet().intersectAndStore(key, otherKeys,
                destKey);
    }

    /**
     * 获取两个集合的并集
     *
     * @param key
     * @param otherKeys
     * @return
     */
    public Set<Object> sUnion(String key, String otherKeys) {
        return redisTemplate.opsForSet().union(key, otherKeys);
    }

    /**
     * 获取key集合与多个集合的并集
     *
     * @param key
     * @param otherKeys
     * @return
     */
    public Set<Object> sUnion(String key, Collection<String> otherKeys) {
        return redisTemplate.opsForSet().union(key, otherKeys);
    }

    /**
     * key集合与otherKey集合的并集存储到destKey中
     *
     * @param key
     * @param otherKey
     * @param destKey
     * @return
     */
    public Long sUnionAndStore(String key, String otherKey, String destKey) {
        return redisTemplate.opsForSet().unionAndStore(key, otherKey, destKey);
    }

    /**
     * key集合与多个集合的并集存储到destKey中
     *
     * @param key
     * @param otherKeys
     * @param destKey
     * @return
     */
    public Long sUnionAndStore(String key, Collection<String> otherKeys,
                               String destKey) {
        return redisTemplate.opsForSet().unionAndStore(key, otherKeys, destKey);
    }

    /**
     * 获取两个集合的差集
     *
     * @param key
     * @param otherKey
     * @return
     */
    public Set<Object> sDifference(String key, String otherKey) {
        return redisTemplate.opsForSet().difference(key, otherKey);
    }

    /**
     * 获取key集合与多个集合的差集
     *
     * @param key
     * @param otherKeys
     * @return
     */
    public Set<Object> sDifference(String key, Collection<String> otherKeys) {
        return redisTemplate.opsForSet().difference(key, otherKeys);
    }

    /**
     * key集合与otherKey集合的差集存储到destKey中
     *
     * @param key
     * @param otherKey
     * @param destKey
     * @return
     */
    public Long sDifference(String key, String otherKey, String destKey) {
        return redisTemplate.opsForSet().differenceAndStore(key, otherKey,
                destKey);
    }

    /**
     * key集合与多个集合的差集存储到destKey中
     *
     * @param key
     * @param otherKeys
     * @param destKey
     * @return
     */
    public Long sDifference(String key, Collection<String> otherKeys,
                            String destKey) {
        return redisTemplate.opsForSet().differenceAndStore(key, otherKeys,
                destKey);
    }

    /**
     * 获取集合所有元素
     *
     * @param key
     * @return
     */
    public Set<Object> setMembers(String key) {
        return redisTemplate.opsForSet().members(key);
    }

    /**
     * 随机获取集合中的一个元素
     *
     * @param key
     * @return
     */
    public String sRandomMember(String key) {
        return (String) redisTemplate.opsForSet().randomMember(key);
    }

    /**
     * 随机获取集合中count个元素
     *
     * @param key
     * @param count
     * @return
     */
    public List<Object> sRandomMembers(String key, long count) {
        return redisTemplate.opsForSet().randomMembers(key, count);
    }

    /**
     * 随机获取集合中count个元素并且去除重复的
     *
     * @param key
     * @param count
     * @return
     */
    public Set<Object> sDistinctRandomMembers(String key, long count) {
        return redisTemplate.opsForSet().distinctRandomMembers(key, count);
    }

    /**
     * @param key
     * @param options
     * @return
     */
    public Cursor<Object> sScan(String key, ScanOptions options) {
        return redisTemplate.opsForSet().scan(key, options);
    }

    /**------------------zSet相关操作--------------------------------*/

    /**
     * 添加元素,有序集合是按照元素的score值由小到大排列
     *
     * @param key
     * @param value
     * @param score
     * @return
     */
    public Boolean zAdd(String key, String value, double score) {
        return redisTemplate.opsForZSet().add(key, value, score);
    }

    /**
     * @param key
     * @param values
     * @return
     */
    public Long zAdd(String key, Set<ZSetOperations.TypedTuple<Object>> values) {
        return redisTemplate.opsForZSet().add(key, values);
    }

    /**
     * @param key
     * @param values
     * @return
     */
    public Long zRemove(String key, Object... values) {
        return redisTemplate.opsForZSet().remove(key, values);
    }

    /**
     * 增加元素的score值,并返回增加后的值
     *
     * @param key
     * @param value
     * @param delta
     * @return
     */
    public Double zIncrementScore(String key, String value, double delta) {
        return redisTemplate.opsForZSet().incrementScore(key, value, delta);
    }

    /**
     * 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列
     *
     * @param key
     * @param value
     * @return 0表示第一位
     */
    public Long zRank(String key, Object value) {
        return redisTemplate.opsForZSet().rank(key, value);
    }

    /**
     * 返回元素在集合的排名,按元素的score值由大到小排列
     *
     * @param key
     * @param value
     * @return
     */
    public Long zReverseRank(String key, Object value) {
        return redisTemplate.opsForZSet().reverseRank(key, value);
    }

    /**
     * 获取集合的元素, 从小到大排序
     *
     * @param key
     * @param start 开始位置
     * @param end   结束位置, -1查询所有
     * @return
     */
    public Set<Object> zRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().range(key, start, end);
    }

    /**
     * 获取集合元素, 并且把score值也获取
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Set<ZSetOperations.TypedTuple<Object>> zRangeWithScores(String key, long start,
                                                                   long end) {
        return redisTemplate.opsForZSet().rangeWithScores(key, start, end);
    }

    /**
     * 根据Score值查询集合元素
     *
     * @param key
     * @param min 最小值
     * @param max 最大值
     * @return
     */
    public Set<Object> zRangeByScore(String key, double min, double max) {
        return redisTemplate.opsForZSet().rangeByScore(key, min, max);
    }

    /**
     * 根据Score值查询集合元素, 从小到大排序
     *
     * @param key
     * @param min 最小值
     * @param max 最大值
     * @return
     */
    public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key,
                                                                          double min, double max) {
        return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max);
    }

    /**
     * @param key
     * @param min
     * @param max
     * @param start
     * @param end
     * @return
     */
    public Set<ZSetOperations.TypedTuple<Object>> zRangeByScoreWithScores(String key,
                                                                          double min, double max, long start, long end) {
        return redisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max,
                start, end);
    }

    /**
     * 获取集合的元素, 从大到小排序
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Set<Object> zReverseRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().reverseRange(key, start, end);
    }

    /**
     * 获取集合的元素, 从大到小排序, 并返回score值
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeWithScores(String key,
                                                                          long start, long end) {
        return redisTemplate.opsForZSet().reverseRangeWithScores(key, start,
                end);
    }

    /**
     * 根据Score值查询集合元素, 从大到小排序
     *
     * @param key
     * @param min
     * @param max
     * @return
     */
    public Set<Object> zReverseRangeByScore(String key, double min,
                                            double max) {
        return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max);
    }

    /**
     * 根据Score值查询集合元素, 从大到小排序
     *
     * @param key
     * @param min
     * @param max
     * @return
     */
    public Set<ZSetOperations.TypedTuple<Object>> zReverseRangeByScoreWithScores(
            String key, double min, double max) {
        return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key,
                min, max);
    }

    /**
     * @param key
     * @param min
     * @param max
     * @param start
     * @param end
     * @return
     */
    public Set<Object> zReverseRangeByScore(String key, double min,
                                            double max, long start, long end) {
        return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max,
                start, end);
    }

    /**
     * 根据score值获取集合元素数量
     *
     * @param key
     * @param min
     * @param max
     * @return
     */
    public Long zCount(String key, double min, double max) {
        return redisTemplate.opsForZSet().count(key, min, max);
    }

    /**
     * 获取集合大小
     *
     * @param key
     * @return
     */
    public Long zSize(String key) {
        return redisTemplate.opsForZSet().size(key);
    }

    /**
     * 获取集合大小
     *
     * @param key
     * @return
     */
    public Long zZCard(String key) {
        return redisTemplate.opsForZSet().zCard(key);
    }

    /**
     * 获取集合中value元素的score值
     *
     * @param key
     * @param value
     * @return
     */
    public Double zScore(String key, Object value) {
        return redisTemplate.opsForZSet().score(key, value);
    }

    /**
     * 移除指定索引位置的成员
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public Long zRemoveRange(String key, long start, long end) {
        return redisTemplate.opsForZSet().removeRange(key, start, end);
    }

    /**
     * 根据指定的score值的范围来移除成员
     *
     * @param key
     * @param min
     * @param max
     * @return
     */
    public Long zRemoveRangeByScore(String key, double min, double max) {
        return redisTemplate.opsForZSet().removeRangeByScore(key, min, max);
    }

    /**
     * 获取key和otherKey的并集并存储在destKey中
     *
     * @param key
     * @param otherKey
     * @param destKey
     * @return
     */
    public Long zUnionAndStore(String key, String otherKey, String destKey) {
        return redisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey);
    }

    /**
     * @param key
     * @param otherKeys
     * @param destKey
     * @return
     */
    public Long zUnionAndStore(String key, Collection<String> otherKeys,
                               String destKey) {
        return redisTemplate.opsForZSet()
                .unionAndStore(key, otherKeys, destKey);
    }

    /**
     * 交集
     *
     * @param key
     * @param otherKey
     * @param destKey
     * @return
     */
    public Long zIntersectAndStore(String key, String otherKey,
                                   String destKey) {
        return redisTemplate.opsForZSet().intersectAndStore(key, otherKey,
                destKey);
    }

    /**
     * 交集
     *
     * @param key
     * @param otherKeys
     * @param destKey
     * @return
     */
    public Long zIntersectAndStore(String key, Collection<String> otherKeys,
                                   String destKey) {
        return redisTemplate.opsForZSet().intersectAndStore(key, otherKeys,
                destKey);
    }

    /**
     * @param key
     * @param options
     * @return
     */
    public Cursor<ZSetOperations.TypedTuple<Object>> zScan(String key, ScanOptions options) {
        return redisTemplate.opsForZSet().scan(key, options);
    }

}

4.1.4,根据数据库构建相对应的实体类,在service层上编写相关业务。

dao层上的简单操作

package com.yy.service;

import com.yy.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Repository;

@Mapper
@Repository
public interface US {
    @Select("select * from user where id=#{id}")
    User selectUserById(Integer id);
}

业务类上进行实现

package com.yy.service;

import com.yy.pojo.User;
import com.yy.utils.RedisUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class ServiceImpl {
    @Autowired
    private US us;
    @Autowired
    RedisUtils redisUtils;

    public User select(Integer id) {
        if (Boolean.FALSE.equals(redisUtils.hasKey(String.valueOf(id)))) {
            log.info("查询mysql数据库");
            User user = us.selectUserById(id);
            user.setId(id);
            user.setUsername("张三");
            user.setPwd("147852");
            redisUtils.hPut("user", String.valueOf(id), String.valueOf(user));
            return user;
        } else {
            log.info("查询redis");
            return (User) redisUtils.hGet("user", String.valueOf(id));
        }

    }
}

4.1.5,测试

编写一个测试类进行测试:

@Test
    void test() {
        System.out.println(service.select(1002));
    }

 运行结果可以看到数据已经查询出来,并且将数据存储在redis中了

 但是这种方式,代码量和操作都不太方便。因此,在项目中我跟推荐使用注解去实现。

4.2,注解实现缓存

因为导入依赖与上面大致一样,需要注意的是我在这个新的测试上使用了Mybatis-plus插件,所以依赖上还需要加上相关依赖,其他地方不重复详述。

4.2.1,构建数据库并配置yml文件

server:
  port: 8090
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    password: 123456
    username: root
    url: jdbc:mysql://localhost:3306/db_user?&useSSL=true&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
  redis:
    database: 0
    port: 6379
    host: redis的ip地址
    timeout: 50000
    lettuce:
      pool:
        max-active: 8 #连接池最大连接数
        max-wait: -1 #最大阻塞等待时间(负数表示无限制)
        max-idle: 8 #最大空闲连接数
    password: 123456
  aop:
    proxy-target-class: true

mybatis-plus:
  mapper-locations: classPath:/mapper/*.xml
  configuration:
    map-underscore-to-camel-case: false # 禁止大写变小写时自动添加下划线
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  type-aliases-package: com.yy.pojo

为了测试一下缓存效果,这次笔者重新构建了一个数据库,并向数据库中导入10万条测试数据。以备后面测试缓存读取数据时的效果。

4.2.2,配置缓存管理器

package com.yy.redisCache.util;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
public class RedisConfig {
    /**
     * 实例化具体的缓存配置!
     *    设置缓存方式JSON
     *    设置缓存时间 单位秒
     * @param ttl
     * @return
     */
    private RedisCacheConfiguration redisCacheConfiguration(Long ttl){
        //设置jackson序列化工具
        Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        //常见jackson的对象映射器,并设置一些基本属性
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        objectJackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofSeconds(ttl))//设置缓存时间
                .disableCachingNullValues()
                .serializeKeysWith(keyPair())
                .serializeValuesWith(valuePair());
    }
    //配置缓存管理器
    @Bean("cacheManagerHour")
    @Primary  //同类型,多个bean,默认生效! 默认缓存时间1小时!  可以选择!
    public CacheManager cacheManagerHour(RedisConnectionFactory factory){
        //缓存时间一小时
        RedisCacheConfiguration redisCacheConfiguration = redisCacheConfiguration( 3600L);
        //构建缓存对象
        return RedisCacheManager.builder(factory)
                .cacheDefaults(redisCacheConfiguration)
                .transactionAware()
                .build();
    }

    //缓存一天的配置
    @Bean(name ="cacheManagerDay")
    public RedisCacheManager cacheManagerDay(RedisConnectionFactory factory){
        //缓存时间为一天
        RedisCacheConfiguration redisCacheConfiguration = redisCacheConfiguration(24 * 3600L);
        return RedisCacheManager.builder(factory)
                .cacheDefaults(redisCacheConfiguration)
                .transactionAware()
                .build();
    }

    /**
     * 配置键序列化
     * @return String序列化
     */
    @Bean
    RedisSerializationContext.SerializationPair<String> keyPair(){
        return RedisSerializationContext.SerializationPair.fromSerializer( new StringRedisSerializer());
    }

    /**
     * 配置值序列化
     * @return GenericJackson2JsonRedisSerializer序列化Object
     */
    @Bean
    RedisSerializationContext.SerializationPair<Object> valuePair(){
        return RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer());
    }
}

4.2.3,开启注解支持

在启动类上添加@EnableCache,开启缓存功能

package com.yy;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableCaching//开启缓存
@MapperScan({"com.yy.dao","com.yy.redisCache.dao"})
@EnableTransactionManagement
public class SpringbootMongodbStudyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootMongodbStudyApplication.class, args);
    }

}

4.2.4,SpringCache的一些相关注解说明

类型名称解释
注解

@CacheConfig

一般配置在指定类上用于指定缓存名称,并且名称与缓存管理器中的缓存名保持一致
@Cacheable可标记在方法上,也可以标记在类上。标记在上时表示该类的所有方法都支持缓存。标记在方法上时,该方法的返回结果将会进行缓存。如果已经存在该缓存,则会直接从缓存中读取数据。可以通过key指定入参,value为方法返回值缓存在得 Cache名称
@CachePut可以标注在类上和方法上。无论是否存在缓存,每次都会重新添加缓存,常用于更新。
@CacheEvit可以标记在一个方法上,也可以标记在一个类上。标记方法上标识方法执行时候触发清楚缓存操作,当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。
@Caching可以让我们在一个方法或者类上同时指定多个Spring Cache相关的注解。
注解配置参数value,cacheNames指定(缓存管理器) Cache名称,效果类似于@CacheConfig中的指定名称。如果已经用@CacheConfig在类上指定了,方法中就不需要使用了,就像提取公共名称一样
key缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合。未设置则为默认值。
condition缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存
unless可以用来是决定是否添加到缓存,与 condition不同的是,unless表达式是在方法调用之后进行评估的。如果返回false,才放入缓存 (与condition相反)
sync在多线程环境下,某些操作可能使用相同参数同步调用。默认情况下,缓存不锁定任何资源,可能导致多次计算,而违反了缓存的目的。对于这些特定的情况,属性 sync 可以指示底层将缓存锁住,使只有一个线程可以进入计算,而其他线程堵塞,直到返回结果更新到缓存中

EL表达式可以使用方法参数及对应的属性。使用方法参数时,可以直接使用 "#参数名" 或者 "#p参数index9(参数下标)",#result为返回值

4.2.5,创建pojo类,编写dao与mapper.xml

package com.yy.redisCache.pojo;

import lombok.Data;

import java.io.Serializable;

/**
 * @author young
 * @date 2022/9/4 13:02
 * @description:
 */
@Data
public class RedisTest implements Serializable {
    private static final long serialVersionUID = -7120480143440693836L;
    private Integer id;
    private String redis_name;
    private String redis_pwd;
    private String address;
}

4.2.6,编写业务接口及其实现类

package com.yy.redisCache.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.yy.redisCache.pojo.RedisTest;

/**
 * @author young
 * @date 2022/9/4 13:06
 * @description: 业务接口
 */
public interface RedisTestService extends IService<RedisTest> {
}

在实现类中添加缓存注解,指定的业务的返回结果存入缓存。

package com.yy.redisCache.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yy.redisCache.dao.RedisTestDao;
import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.RedisTestService;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;

/**
 * @author young
 * @date 2022/9/4 13:08
 * @description: service实现类
 */
@Service
@CacheConfig(cacheNames = "test")//缓存名,和管理器中配置的一致
public class RedisTestServiceImpl extends ServiceImpl<RedisTestDao, RedisTest> implements RedisTestService {
    @Resource
    private RedisTestDao redisTestDao;

    /**
     * 查寻10000条数据
     * @return
     * key 可为空,指定需按照SpEL编写
     * unless 不缓存的条件 条件为true不缓存
     */
    @Override
    @Cacheable(key = "'allUser'",unless = "#result==null")//#result返回结果
    public List<RedisTest> list() {
        QueryWrapper<RedisTest> wrapper = new QueryWrapper<>();
        wrapper.le("id",10000);
        return redisTestDao.selectList(wrapper);
    }


    /**
     * 批量查询
     * @param idList
     * @return
     */
    @Override
    public List<RedisTest> listByIds(Collection<? extends Serializable> idList) {
        return super.listByIds(idList);
    }
    /**
     * 删除批量信息
     * @param list
     * @return
     */
    @CacheEvict
    @Override
    public boolean removeByIds(Collection<?> list) {
        return super.removeByIds(list);
    }
    
    /**
     * 修改某个对象
     * @param entity
     * @return
     */
    @Override
    @CachePut
    public boolean updateById(RedisTest entity) {
        return super.updateById(entity);
    }

}

4.2.7,编写controller层并进行测试

package com.yy.redisCache.controller;

import com.yy.redisCache.pojo.RedisTest;
import com.yy.redisCache.service.impl.RedisTestServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * @author young
 * @date 2022/9/4 13:13
 * @description:
 */
@RestController
@RequestMapping("/Cache")
@Slf4j
public class RedisCacheController {
    @Autowired
    private RedisTestServiceImpl service;

    /**
     * 删除指定id以下的所有数据及缓存
     * @param id id
     * @return
     */
    @DeleteMapping("/del/{id}")
    public Boolean remove(@PathVariable Integer id){
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i <= id; i++) {
            list.add(i);
        }
       return service.removeByIds(list);
    }

    /**
     * 更新数据
     * @param redisTest 更新对象
     * @return
     */
    @PutMapping("/update")
    public Boolean update(RedisTest redisTest){
        return service.updateById(redisTest);
    }

    /**
     * 测试从数据库/缓存获取10000条数据的时间
     * @return
     */
    @GetMapping("/get")
    public List<RedisTest> getBody(){
        Long startTime = System.currentTimeMillis();
        List<RedisTest> list = service.list();
        //第一次存入数据库查询3875ms  第二次缓存查询:1131ms
        log.info("查询时间10000条数据的时间是:{}",System.currentTimeMillis()-startTime);
        return list;
    }

    /**
     * 批量获取指定的数据
     * @param id
     * @return
     */
    @GetMapping("/getOne/{id}")
    public String getOne(@PathVariable Integer... id){
        List<RedisTest> ids = service.listByIds(Arrays.asList(id));
        return JSONUtil.toJsonStr(ids);
    }
}

通过接口测试工具测试获取10000条数据的测试后,可以看到第一次获取10000条数据后,时间为8330ms,并且redis里已经有了相关缓存。

 再次执行该请求后,并不会执行sql查询操作(因为已经不打印查询日志了)获取时间大大减少为1131ms。此时,表示是从缓存读取的数据。

可以猜想到,由于数据库执行查询操作时,需要进行数据库连接等操作,因次直接从数据库获取数据的时间会比redis缓存慢。最后我们测试一下更新删除缓存等操作,成功达到我们的预期结果。 

使用问题说明 

使用注解来实现缓存虽然简便,但是很多时候如果使用不注意,并不能实现缓存效果,这里对一些场景下的使用做一些简单说明,给小伙伴们避避坑!

1,对于已放入缓存的集合数据而言,存入缓存的数据并不能影响单次查询数据库。以及就是说,我先查询了所有数据,并放入缓存,此时缓存名为该集合对应的key。当我们查询其中的单个数据时,它并不会在集合中查,而是走数据库,因为查询缓存也是通过key查value的,也就是存放在缓存中的数据。

除非你单次查询也设置了缓存,并标名了唯一key。比如我查一次数据后,将数据id作为key,查询的结果作为value存入缓存。

    @Cacheable(key = "#id")
    public RedisTest findById(Integer id) {
        return redisTestDao.findAllByIdRedisTest(id);
    }

 这样每次查一个数据后,后续就会走缓存,而不是每次查数据库。而集合的查询作用是什么呢,单纯提高批量查询的效率罢了,避免每次批量查询数据库。而且如果查询缓存没命中,它也会查数据库,然后再以新的key存对应数据。所以说基本与单次查询无关,除非你把单次查询的key和集合中的key关联了。

2,使用@CachePut需要格外注意!虽然该注解用于更新缓存,但是它返回的是该注解标名的方法返回值。这就是说,如果你update方法返回的是boolean或者int,那么它将覆盖之前的缓存中的key对应的对象数据。演示如下:

先通过fingById获取一个id为7的数据,并存入缓存:

此时缓存中已存在该数据的缓存,下次再次查询id为7的数据时就直接走缓存了。然后执行一个更新,更新对象对应id为7,但是业务逻辑返回值为boolean类型。

    @Override
    @CachePut(key = "#entity.id")
    public boolean updateById(RedisTest entity) {
        return super.updateById(entity);
    }

 执行更新操作后,通过日志可以查看到首先执行的是数据库更新,然后将结果放入缓存,此时查看缓存,发现此时缓存里test::7里面的数据变为true了。

然后如果我们执行查询操作,查询id为7的数据时,控制台直接报错,类型转换异常。

2023-04-25 15:24:12.555  INFO 7820 --- [nio-8090-exec-7] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T15:24:12.555--访问接口:RedisTest com.yy.redisCache.controller.RedisCacheController.getById(Integer)
2023-04-25 15:24:12.582 ERROR 7820 --- [nio-8090-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ClassCastException: java.lang.Boolean cannot be cast to com.yy.redisCache.pojo.RedisTest] with root cause

java.lang.ClassCastException: java.lang.Boolean cannot be cast to com.yy.redisCache.pojo.RedisTest
	at com.yy.redisCache.service.impl.RedisTestServiceImpl$$EnhancerBySpringCGLIB$$1.findById(<generated>) ~[classes/:na]
	at com.yy.redisCache.controller.RedisCacheController.getById(RedisCacheController.java:66) ~[classes/:na]
	at com.yy.redisCache.controller.RedisCacheController$$FastClassBySpringCGLIB$$1.invoke(<generated>) ~[classes/:na]

显而易见,它直接将缓存中的true拿去执行查询了。所以为了避免这种问题,我们需要将更新业务逻辑做简单处理,保证更新后得到的是个新的对象。比如我们更新后再查询一次,并返回结果:

@Override
    @CachePut(key = "#redisTest.getId()")
    public RedisTest updateMsg(RedisTest redisTest) {
        boolean b = redisTestDao.updateBody(redisTest);
        if (b){
           return redisTestDao.selectById(redisTest.getId());
        }return null;
    }

这样缓存中存放的就是更新后的数据。

2023-04-25 15:45:50.157  INFO 7820 --- [nio-8090-exec-3] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T15:45:50.157--访问接口:RedisTest com.yy.redisCache.controller.RedisCacheController.putOne(RedisTest)
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@b58e587] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@963850027 wrapping com.mysql.cj.jdbc.ConnectionImpl@6d091153] will not be managed by Spring
==>  Preparing: update redis_test set redis_name=?,redis_pwd=?,address=? where id =?
==> Parameters: 张无忌(String), 777777(String), 光明顶(String), 7(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@b58e587]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@18ed8357] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1423255824 wrapping com.mysql.cj.jdbc.ConnectionImpl@6d091153] will not be managed by Spring
==>  Preparing: SELECT id,redis_name,redis_pwd,address FROM redis_test WHERE id=?
==> Parameters: 7(Integer)
<==    Columns: id, redis_name, redis_pwd, address
<==        Row: 7, 张无忌, 777777, 光明顶
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@18ed8357]

#执行查询id为7的操作,在缓存中查询数据,不走数据库
2023-04-25 15:46:16.670  INFO 7820 --- [nio-8090-exec-2] com.yy.Aspect.LogAspect                  : 访问时间:2023-04-25T15:46:16.670--访问接口:RedisTest com.yy.redisCache.controller.RedisCacheController.getById(Integer)

 后续我们再次查询id为7的数据时,此时缓存中已有更新后的数据,就能直接在缓存中获取啦。

所以@CachePut实现的是先更新数据库,再放入缓存的操作 

3,在同一个业务方法内调用被缓存注解标识的方法时,被标识的方法中的缓存效果失效。举例:

@Override
    @CachePut(key = "#redisTest.getId()")
    public RedisTest updateMsg(RedisTest redisTest) {
        boolean b = redisTestDao.updateBody(redisTest);
        if (b){
           return findById(redisTest.getId()-4);
        }return null;
    }

 此时缓存中存在id为11,7的数据,我们更新11中的数据,然后却返回一个id为7的数据(很蛋疼的逻辑,只不过测试一下效果而已)。并且在findById这个方法中已经用@Cacheable标识了,但是实际上在调用这个updateMsg方法时并不会让findById走缓存。它仍然会查询数据库。

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a226c4] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1512111894 wrapping com.mysql.cj.jdbc.ConnectionImpl@71f86c11] will not be managed by Spring
==>  Preparing: update redis_test set redis_name=?,redis_pwd=?,address=? where id =?
==> Parameters: 张无忌(String), 66666(String), 光明顶(String), 11(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@77a226c4]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@61ba0085] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1010797547 wrapping com.mysql.cj.jdbc.ConnectionImpl@71f86c11] will not be managed by Spring
==>  Preparing: select * from redis_test where id =?
==> Parameters: 7(Integer)
<==    Columns: id, redis_name, redis_pwd, address
<==        Row: 7, 张无忌, 777777, 光明顶
<==      Total: 1

所以这也是需要注意的,存在缓存失效的可能。 

4,缓存一致性问题!这个问题也是用注解缓存经常容易出现坑的地方,比如批量查询全部数据并放入缓存,key为list,但是之后我们修改了其中某条单个数据,这时候数据库执行了更新操作,但是当我们再次请求批量查询时,由于存在缓存list,便不会走数据库而是直接查缓存并返回旧数据,导致新旧数据不一致,这就出现了所谓的缓存一致性问题,因此对于经常需要修改的数据并不建议使用注解缓存偷懒,特别是涉及到高并发的环境下更要格外注意,如果数据一致性要求不高或者不经常变动只需要做读操作就可以放心使用啦。

五,缓存中常见问题

5.1,缓存穿透

缓存穿透是指查询一个不存在的数据,在缓存层和持久层都查询不到,这样查不到的数据不会写入缓存,这样一个不存在的数据每次的请求都会去查询持久层,缓存也就失去了保护后端持久层的意义了。最严重的后果就是是后端存储负载加大,造成后端存储宕机。

形成原因:

  • 业务代码或数据出现问题,比如读写key不一致
  • 网络爬虫或恶意攻击

解决方法:

1,缓存空对象:对于查询不到的数据给它设置一个空值。但是可能会造成内存空间紧张,因为value为null也会占用内存空间,不过我们可以给它设置一个过期时间,让空值缓存自动过期。这种方法使用与数据经常变化,实时性较高的场景。

2,布隆过滤器拦截:在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截,当收到一个对key请求时先用布隆过滤器验证是key否存在,如果存在在进入缓存层、存储层。可以使用bitmap做布隆过滤器。这种方法适用于数据命中不高、数据相对固定、实时性低的应用场景,代码维护较为复杂,但是缓存空间占用少。

5.2,缓存击穿

某个key成为一个热点(商品秒杀时)时,这样处于一个集中式高并发的情况下,如果key突然失效一瞬间,请求就会马上击穿缓存层,直接请求数据库,这样后端负载会很快过载甚至崩溃。

解决办法:

1,根据实际情况设置二级缓存或者热点缓存永不过期

2,设置分布式互斥锁,只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据

5.3,缓存雪崩

当缓存层由于某些原因不可用(宕机)或者大量缓存由于超时时间相同在同一时间段失效(大批key失效/热点数据失效),大量请求直接到达存储层,存储层压力过大导致系统雪崩。

解决办法:

1,设置多级缓存,不同的缓存设置不同的过期时间。

2,将缓存层设计为高可用,例如使用sentinel哨兵模式或者cluster,这样即使个别节点或机器宕机也不会影响缓存效果实现。

3,将缓存的过期时间设置随机分布的,避免在同一时间内缓存集体失效。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值