【12】SpringBoot-Redis相关

【12】SpringBoot-Redis相关

文章目录

    • 【12】SpringBoot-Redis相关
      • 一、spring-data-redis 简介
        • 1.1 RedisTemplate
        • 1.2 Spring 对 Redis数据类型操作
        • 1.3 SessionCallback 和 RedisCallback 接口
      • 二、SpringBoot 中配置 Redis
        • 2.1 操作 redis字符串和散列数据类型
        • 2.2 操作列表(链表
        • 2.3 操作集合
        • 2.4 操作有序集合
      • 三、Redis 特殊用法
        • 3.1 使用 Redis事务
        • 3.2 Redis 流水线
        • 3.3 使用 Redis发布订阅
        • 3.4 使用Lua脚本
      • 四、Spring 缓存注解操作redis
        • 4.1 缓存管理器和缓存的启用
        • 4.2 开发缓存注解
        • 4.3 缓存注解自调用失效问题
        • 4.4 自定义缓存管理器

一、spring-data-redis 简介

Spring 提供了一个 RedisConnectionFactory 接口,通过它可以生成一个RedisConnection接口对象, 而 RedisConnection 接口对象是对 Redis底层接口的封装。例如,下面使用的 Jedis 驱动,Spring 就会提供 RedisConnection 接口的实现类 JedisConnection 去封装原有的 Jedis(redis.clients.jedis.Jedis)对象。

查看类图

【12】SpringBoot-Redis相关_第1张图片

从图可看出,Spring中是通过 RedisConnection接口操作 Redis的,而RedisConnection 则对原生的 Jedis进行封装。要获取 RedisConnection接口对象,是通过 RedisConnectionFactory 接口去生成的,

所以第一步要配置这个redis连接工厂,配置这个工厂主要是配置 Redis的连接池,对于连接池可以限定其最大连接数、超时时间等属性。如下

@Configuration       // 都可以在springboot配置文件中配置
public class RedisConfig {

    private RedisConnectionFactory connectionFactory;

    // 自定义配置redis连接工厂
    @Bean(name = "redisConnectionFactory")
    public RedisConnectionFactory initRedisConnectionFactory() {
        if (Objects.nonNull(this.connectionFactory)) {
            return this.connectionFactory;
        }

        JedisPoolConfig poolConfig = new JedisPoolConfig();
        // 最大空闲数
        poolConfig.setMaxIdle(30);
        // 最大连接数
        poolConfig.setMaxTotal(50);
        // 最大等待毫秒数
        poolConfig.setMaxWaitMillis(2000);
        // 创建Jedis连接工厂
        JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
        
        // 获取单机的redis配置
        RedisStandaloneConfiguration rsCfg = connectionFactory.getStandaloneConfiguration();
        connectionFactory.setHostName("192.168.11.131");
        connectionFactory.setPort(6379);
        connectionFactory.setPassword("123456");
        this.connectionFactory = connectionFactory;
        return connectionFactory;
    }
    
    // ...
}

1.1 RedisTemplate

RedisTemplate 是使用最多的类,它会自动从 RedisConnectionFactory 工厂中获取连接,然后执行对应的 Redis命令,最后关闭 Redis连接。

对象是无法存储到redis中的,不过 Java 提供了序列化机制,只要类实现了 Serializable接口,就能对类对象进行序列化就能得到二进制字符串,然后Redis 就可以将这些类对象以字符串进行存储。并且可以从redis中取出进行反序列化为对象。Spring中,提供了序列化器的机制,并且实现了几个序列化器,如图

【12】SpringBoot-Redis相关_第2张图片

查看序列化器接口

public interface RedisSerializer<T> {
    
    // 序列化对象
    @Nullable
    byte[] serialize(@Nullable T var1) throws SerializationException;

    // 反序列化
    @Nullable
    T deserialize(@Nullable byte[] var1) throws SerializationException;

    // ...
}

  • StringRedisSeralizer 是针对字符串

  • JdkSerializationRedisSerializer 是 RedisTemplate 的默认序列化器。如果使用这个序列化器存储键值对,那么取键的时候并不是原来的键。(比如存储了一个 key1 ,通过 keys *key1。可看到存的是 “/xac/xbc/…/key1"这样一个键。这样我们取 redis 的数据就比较困难

RedisTemplate 可配置的属性:

属性 描述 备注

如何解决?把 redis的键用 普通字符串保存,如下配置

    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> initRedisTemplate(){
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

        // RedisTemplate 会自动初始化 StringRedisSerializer ,所以这里直接获取
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();

        // 设置字符串序列化器,这样 Spring就会把 Redis的key当做字符串处理了
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
        redisTemplate.setConnectionFactory(initRedisConnectionFactory());
        return redisTemplate;
    }

1.2 Spring 对 Redis数据类型操作

Redis 可支持 7种类型的数据结构,这7种是 字符串、散列、列表(链表)、集合、有序集合、基数和地理位置。Spring 为每一种都封装了对应的操作

        // 列表操作
        ListOperations listOps = redisTemplate.opsForList();
        // 字符串
        ValueOperations valueOps = redisTemplate.opsForValue();
        // 普通set集合
        SetOperations setOps = redisTemplate.opsForSet();
        // 有序set集合
        ZSetOperations zSetOps = redisTemplate.opsForZSet();
        // 散列操作接口
        HashOperations hashOps = redisTemplate.opsForHash();

        // 地理位置操作接口
        GeoOperations geoOps = redisTemplate.opsForGeo();
        // 基数操作接口
        HyperLogLogOperations hyperLogLogOps = redisTemplate.opsForHyperLogLog();

有时需连续多次操作一个散列类型或列表,可使用 “绑定对象” ,即 BoundXXXOperations 接口,如下

        // Geo
        BoundGeoOperations geoKey = redisTemplate.boundGeoOps("geoKey");
        // hash
        BoundHashOperations hashKey = redisTemplate.boundHashOps("hashKey");
		// list
        BoundListOperations list = redisTemplate.boundListOps("list");
        // set
        BoundSetOperations setKey = redisTemplate.boundSetOps("setKey");
        // string
        BoundValueOperations stringKey = redisTemplate.boundValueOps("stringKey");
        // zset
        BoundZSetOperations zsetKey = redisTemplate.boundZSetOps("zsetKey");

1.3 SessionCallback 和 RedisCallback 接口

这2个接口的作用:让 RedisTemplate 进行回调,通过它们可以在一条连接执行多个 Redis 命令SesssionCallback 封装比较好,推荐使用。RedisCallback 接口比较底层,需要处理的内容多,可读性差。

都是通过 RedisTemplate的 execute()方法进行调用的

使用示例:

    /**
     * 使用1个redis连接执行多条语句   SessionCallback :高级接口,推荐使用
     */
    public static void useSessionCallback(RedisTemplate redisTemplate) {
        redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations ro) throws DataAccessException {
                ro.opsForValue().set("key2", "val1");
                ro.opsForHash().put("myHash", "field", "hval33");
                return null;
            }
        });
    }

    /**
     * RedisCallback:需要处理底层的转换规则,如果不考虑改写底层,尽量不要使用
     */
    public static void useRedisCallback(RedisTemplate redisTemplate) {
        redisTemplate.execute((RedisCallback) rc -> {
            rc.set("key3".getBytes(), "val4".getBytes());
            rc.hSet("hash".getBytes(), "field3".getBytes(), "hval8".getBytes());
            return null;
        });
    }

二、SpringBoot 中配置 Redis

springboot 中redis的配置

# 数据库配置
spring:
  datasource:
    username: root
    password: admin
    url: jdbc:mysql:///eesy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    # 配置redis连接池   ------> RedisConnectionFactory 配置的内容
    jedis:
      pool:
        min-idle: 5   # 最小空闲数
        max-active: 10  # 最大活动数
        max-idle: 10  # 最大空闲数
        max-wait: 2000  # 获取连接最大等待时间
    # redis服务器属性
    port: 6379
    host: 127.0.0.1
    # password: 123456
    # redis 连接池超时时间,单位毫秒
    timeout: 1000

通过上面的配置,SpringBoot 就可以自动配置 Redis相关的操作对象。比如 RedisConnectionFactory,RedisTemplate,StringRedisTemplate

然后配置序列化器,在 SpringBoot 启动类下配置

@SpringBootApplication
@EnableCaching  // 使用注解缓存驱动机制
public class SpringbootDataMybatis01Application {

    @Autowired
    private RedisTemplate redisTemplate;

    // 定义 Spring-自定义后初始化方法
    @PostConstruct
    public void init() {
        this.initRedisTemplate();
    }

    public void initRedisTemplate() {
        // RedisTemplate 默认定义了一个 StringRedisSerializer
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();

        // 设置字符串序列化器,这样 Spring就会把 Redis的key当做字符串处理了
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
    }

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

2.1 操作 redis字符串和散列数据类型

    @RequestMapping("/string-hash")
    public String testStringAndHash() {
        redisTemplate.opsForValue().set("key1", "val1");

        // 默认,使用jdk的序列化器,所以redis保存时不是整数,不能运算
        redisTemplate.opsForValue().set("int_key", "1");

        // 使用 字符串型的序列化器
        stringRedisTemplate.opsForValue().set("int", "1");
        // 运算+1
        stringRedisTemplate.opsForValue().increment("int", 1);
        // 获取底层jedis连接
        Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
        // 减1操作,这个操作RedisTemplate不支持
        jedis.decr("int");

        // ####################### hash操作
        HashMap<String, String> hash = new HashMap<>();
        hash.put("field1", "val1");
        hash.put("field2", "val2");

        // 存入一个散列数据类型
        stringRedisTemplate.opsForHash().putAll("hash", hash);
        // 新增字段
        stringRedisTemplate.opsForHash().put("hash", "field3", "val3");
        
        
        // 绑定散列操作的key,连续对一个key操作
        BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps("hash");
        // 删除2个字段
        hashOps.delete("field1", "field2");
        // 新增字段
        hashOps.put("field4", "val4");

        return "success";
    }

2.2 操作列表(链表

    @RequestMapping("/redis-list")
    public String testList() {
        // v4 v3 v2 v1  从左边插入
        stringRedisTemplate.opsForList().leftPushAll("list1", "v1", "v2", "v3", "v4");

        // v1 v2 v3 v4
        stringRedisTemplate.opsForList().rightPushAll("list2", "v1", "v2", "v3", "v4");

        // 绑定list2链表操作
        BoundListOperations<String, String> listOps = stringRedisTemplate.boundListOps("list2");
        // 从右边弹出一个成员
        String v4 = listOps.rightPop();
        // 获取定位元素
        String v2 = listOps.index(1);

        // 从左边插入链表
        Long v0 = listOps.leftPush("v0");

        // 链表长度
        Long len = listOps.size();

        // 求链表下标区间成员,范围[0,len-1]
        List<String> elements = listOps.range(0, len - 2);
        elements.forEach(System.out::print);

        return "success";
    }

2.3 操作集合

    @RequestMapping("/redis-set")
    public String testSet() {
        stringRedisTemplate.opsForSet().add("set1", "v1", "v2", "v3", "v3", "v4");
        stringRedisTemplate.opsForSet().add("set2", "v0", "v2", "v3", "v4", "v5");

        // 绑定set1集合操作
        BoundSetOperations<String, String> setOps = stringRedisTemplate.boundSetOps("set1");
        // 增加2个元素
        setOps.add("v6", "v7");
        // 删除2个元素
        setOps.remove("v1", "v2");
        // 返回所有元素
        Set<String> set1 = setOps.members();
        // 求成员数
        Long size = setOps.size();

        // 求交集
        Set<String> inner = setOps.intersect("set2");
        // 求交集,用新集合inner保存
        setOps.intersectAndStore("set2", "inner");

        // 求差集,用diff保存
        Set<String> diff = setOps.diff("set2");
        setOps.diffAndStore("set2", "diff");

        // 求并集,用新集合 union保存-->redis中
        Set<String> union = setOps.union("set2");
        setOps.unionAndStore("set2", "union");

        return "success";
    }

2.4 操作有序集合

Spring中使用 TypedTuple 保存zSet元素

    /**
     * Spring中使用 TypedTuple保存zSet元素
     */
    @RequestMapping("/redis-zset")
    public String testZset() {

        HashSet<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
        for (int i = 1; i <= 9; i++) {
            double score = i * 0.1;
            ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<>("value" + i, score);
            typedTupleSet.add(typedTuple);
        }

        // 插入元素
        stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
        // 绑定操作
        BoundZSetOperations<String, String> zsetOps = stringRedisTemplate.boundZSetOps("zset1");

        // 添加元素
        zsetOps.add("value10", 0.12);
        Set<String> setRange = zsetOps.range(1, 6);

        // 按分数排序获取有序元素
        Set<String> setScore = zsetOps.rangeByScore(0.2, 0.6);

        // 定义值范围
        RedisZSetCommands.Range range = new RedisZSetCommands.Range();
        range.gt("value3");  // >= value3
        //range.gte("value8");  // < value8
        //range.lt("value8");  // < value8
        //range.lte("value8");  // <= value8

        // 按值排序,字符串排序   结果:value4~9
        Set<String> setLex = zsetOps.rangeByLex(range);

        // 删除元素
        zsetOps.remove("value9", "vlaue2");
        // 求分数
        Double score = zsetOps.score("value8");
        // 在下标区间,按分数排序,返回value和score
        Set<ZSetOperations.TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6);
        // 在分数区间,按分数排序,返回val score    结果:null
        Set<ZSetOperations.TypedTuple<String>> scoreSet = zsetOps.rangeByScoreWithScores(1, 6);

        // 按从大到小排序
        Set<String> reverseSet = zsetOps.reverseRange(2, 8);

        return "success";
    }

三、Redis 特殊用法

3.1 使用 Redis事务

redis 是支持一定事务能力的 NoSQL,在Redis中使用事务,通常的命令组合是 watch…multi…exec,也就是在一个Redis 连接中执行多个命令,这时可考虑使用 SessionCallback接口来做

    // Redis事务测试
    @RequestMapping("/redis-multi")
    public String testMulti() {
        redisTemplate.opsForValue().set("key1", "value1");
        List list = (List) redisTemplate.execute(new SessionCallback() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                // 设置监控key1
                operations.watch("key1");
                // 开启事务,在exec命令执行前,现在只是进入队列
                operations.multi();

                // 将命令放入队列
                operations.opsForValue().set("key2", "value2");
                operations.opsForValue().set("key3", "value3");

                // 获取key2,此时value=null,因为命令还在队列里未执行
                Object val2 = operations.opsForValue().get("key2");
                Object val3 = operations.opsForValue().get("key3");

                return operations.exec();
            }
        });

        System.out.println(list);
        return "redis-multi";
    }

为什么要使用 监控 “key1”,因为根据 redis 的事务规则,我们先监控 key1,然后使用 multi 开启事务,让后面的redis命令进入执行队列,如果在 exec()方法前修改了 key1 ,就会取消事务。没有就可以执行事务

3.2 Redis 流水线

默认情况下,redis命令是一条条命令发送给redis 服务器的,这样显然性能不高。对于redis,可以使用流水线(pipeline)技术,提高redis的性能

 // 使用 redis 流水线
    @RequestMapping("/redis-pipeline")
    public String testPipeline() {
        long start = System.currentTimeMillis();

        List list = redisTemplate.executePipelined(new SessionCallback<Object>() {
            @Override
            public Object execute(RedisOperations operations) throws DataAccessException {
                for (int i = 1; i < 100000; i++) {
                    operations.opsForValue().set("pipeline_" + i, "value_" + i);
                    String value = (String) operations.opsForValue().get("pipeline_" + i);
                    if (i == 100000) {
                        System.out.println("命令只是进入队列,所以值为空【" + value + "】");
                    }
                }
                return null;
            }
        });

        long end = System.currentTimeMillis();
        System.out.println(">>>>>>>>>>>>>>>>>>>>> 耗时:" + (end - start) + " 毫秒");

        return "redis-pipeline";
    }

3.3 使用 Redis发布订阅

发布订阅是消息的一种常见模式。对于 Redis,可提供一个渠道,消息发送到这个渠道,其他系统监听这个渠道。如图

【12】SpringBoot-Redis相关_第3张图片

我们先定义一个消息监听器(MessageListener)

// Redis消息监听器
@Component
public class RedisMessageListener implements MessageListener {
    /**
     * 得到消息后的处理方法,
     * @param message redis发送过来的消息
     * @param pattern 渠道名称
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 消息体
        String body = new String(message.getBody());
        // 渠道名称
        String topic = new String(pattern);
        System.out.println("消息体: "+body);
        System.out.println("渠道名称: "+topic);
    }
}

message参数代表 Redis发送过来的消息,pattern 是消息渠道。

然后去springboot 启动类去配置,使之能够监控 redis的消息,如下

@SpringBootApplication
@EnableCaching  // 使用注解缓存驱动机制
public class SpringbootDataMybatis01Application {

    @Autowired
    private RedisTemplate redisTemplate;

    // Redis连接工厂    --->springboot自动创建
    @Autowired
    private RedisConnectionFactory connectionFactory;

    // Redis消息监听器
    @Autowired
    private MessageListener redisMsgListener;

    // 任务池
    private ThreadPoolTaskScheduler taskScheduler;

    // 定义自定义后初始化方法
    @PostConstruct
    public void init() {
        this.initRedisTemplate();
    }

    public void initRedisTemplate() {
        // RedisTemplate 默认定义了一个 StringRedisSerializer
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();

        // 设置字符串序列化器,这样 Spring就会把 Redis的key当做字符串处理了
        redisTemplate.setKeySerializer(stringSerializer);
        redisTemplate.setHashKeySerializer(stringSerializer);
        redisTemplate.setHashValueSerializer(stringSerializer);
    }

    /**
     * 创建任务池,运行线程,进行阻塞,等待处理redis消息
     */
    @Bean
    public ThreadPoolTaskScheduler initTaskScheduler(){
        if(this.taskScheduler != null){
            return this.taskScheduler;
        }
        taskScheduler = new ThreadPoolTaskScheduler();
        // 设置任务池大小
        taskScheduler.setPoolSize(20);
        return taskScheduler;
    }

    /**
     * 定义Redis的监听容器
     */
    @Bean
    public RedisMessageListenerContainer initRedisContainer(){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();

        // Redis连接池工厂
        container.setConnectionFactory(connectionFactory);
        // 设置运行任务池
        container.setTaskExecutor(this.initTaskScheduler());
        // 定义监听渠道,名称为topic1
        ChannelTopic topic = new ChannelTopic("topic1");
        // 使用 监听器监听Redis的消息
        container.addMessageListener(redisMsgListener,topic);
        return container;
    }

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

上面配置了一个 任务池,线程在里面运行,并进行阻塞,等待redis消息的传入

然后定义了一个 Redis 消息监听容器-RedisMessageListenerContainer,在容器里设置里了 redis连接工厂和指定消息的线程池

-启动SpringBoot项目后,在redis的客户端输入命令

publish topic1 msg

-在Spring中,也可以使用RedisTemplate来发送消息,例如:

// channel 代表渠道,message代表消息
redisTemplate.convertAndSend(channel,message)

3.4 使用Lua脚本

redis 2.6提供了Lua 脚本的支持,而且Lua脚本在 Redis中还具备原子性,所以在需要保证数据一致性的高并发环境中,可以使用 Lua语言来保证数据一致性,而且Lua脚本具有更强的运算功能。而且在高并发保证数据一致性,Lua脚本方案比使用 Redis自身提供的事务要更好一些。

在redis 中有2中运行Lua的方法,一种是直接发送 Lua到 Redis服务器去执行,另一种是先把Lua 发送给Redis,Redis会对 Lua脚本进行缓存,然后返回一个SHA1的32位编码回来,之后只需要发送 SHA1和相关参数给Redis便可以直接执行了。(减少网络传输带来的影响


为了支持Redis 的Lua脚本,Spring 提供了 RedisScript 接口,同时也有一个实现类 DefaultRedisScript 。查看源码

public interface RedisScript<T> {
    // 获取脚本的 sha1
    String getSha1();

    // 获取脚本的返回值
    @Nullable
    Class<T> getResultType();

    // 获取脚本的字符串
    String getScriptAsString();
}

示例1:该脚本简单返回一个字符串

注意:需要定义返回类型,否则没有返回结果

    @RequestMapping("/redis-lua")
    public String testLua() {
        // 创建脚本对象
        DefaultRedisScript<String> rs = new DefaultRedisScript<>();
        // 设置脚本
        rs.setScriptText("return 'Hello Redis'");

        // 定义返回类型,注意:如果没有这个定义,spring不会返回结果
        rs.setResultType(String.class);
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();

        // 执行lua脚本
        String str = (String)redisTemplate.execute(rs, stringSerializer, stringSerializer, null);

        return "redis-lua";
    }

使用 RedisTemplate 的exexute()方法执行脚本。有2种方法执行脚本

//                                   脚本    keys代表redis的键  
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) {
        return this.scriptExecutor.execute(script, keys, args);
    }
//                                      脚本,键的序列化器,参数序列化器,args是脚本参数
    public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) {
        return this.scriptExecutor.execute(script, argsSerializer, resultSerializer, keys, args);
    }

示例2:执行复杂 Lua脚本

lua脚本: 使用了两个键去保存两个参数然后对这两个参数进行比较,如果相等则返回1否则返回0。注意脚本中KEYS[1]和KEYS[2]的写法,它们代表客户端传递的第一个键和第二个键,而ARGV[1]和ARGV[2]则表示客户端传递的第一个和第二个参数。

redis.call('set',KEYS[1],ARGV[1])
redis.call('set',KEYS[2],ARGV[2])
local str1 = redis.call('get',KEYS[1])
local str2 = redis.call('get',KEYS[2])
if str1 == str2 then
return 1
end
return 0
    @RequestMapping("redis-lua2")
    public String testLua2(String key1, String key2, String value1, String value2) {
        // 定义Lua脚本
        String lua = "redis.call('set',KEYS[1],ARGV[1])\n" +
                "redis.call('set',KEYS[2],ARGV[2])\n" +
                "local str1 = redis.call('get',KEYS[1])\n" +
                "local str2 = redis.call('get',KEYS[2])\n" +
                "if str1 == str2 then\n" +
                "return 1\n" +
                "end\n" +
                "return 0";

        // 结果返回为Long
        DefaultRedisScript<Long> rs = new DefaultRedisScript<>();
        rs.setScriptText(lua);
        // 设置结果类型
        rs.setResultType(Long.class);

        // 采用字符串序列化器
        RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
        
        // 定义key参数
        List<String> keyList = new ArrayList<>();
        keyList.add(key1);
        keyList.add(key2);

        // 传递2个参数,分别是key,val 的序列化器
        Long result = (Long) redisTemplate.execute(rs, stringSerializer, stringSerializer, keyList, value1, value2);

        return "redis-lua2";
    }

四、Spring 缓存注解操作redis

4.1 缓存管理器和缓存的启用

Spring 在使用缓存注解前,需要配置缓存管理器,缓存管理器将提供了一些重要信息,如缓存类型、超时时间等。Spring支持多种缓存处理器,提供了CacheManager接口

可配置内容:

SPRING CACHE (Cache Properties)
spring.cache.cache-names-=#如果由底层的缓存管理器支持创建,以逗号分隔的列表来缓存名称
spring.cache.caffeine.spec# caffeine缓存配置细节
spring.cache.couchbase.CaChCOChaS€Xaon=0Couchbase=0mscouchbase缓存超时时间,默认是永不超时 spring.cache.ehcache.config=#配置 ehcache缓存初始化文件路径
spring.cache.infinispan.config=infinispan缓存配置文件
spring.cache.jcache.config=#jcache缓存配置文件
spring.cache.jcache.provider=#jcache缓存提供者配置
spring.cache.redis.cache-valu-null-=true#是否允许 Redis缓存空值
spring.cache.redis.key-prefix-=# Redis的键前缀
spring.cache.redis.time-to-1ive=0ms#缓存超时时间戳,配置为则不设置超时时间  spring.cache.edis.use-key-prefix-=true是否启用 Redis的键前缀
spring.cache.type=#缓存类型在默认的情况下, Spring会自动根据上下文探测

1.配置 Redis缓存管理器

spring.cache.type=REDIS
spring.cache.cache-names=redisCache

这样就配置完了缓存管理器

  • spring.cache.type:配置的是缓存类型,为 Redis, SpringBoot 会自动生成 RedisCacheManager对象,
  • spring.cache.cache-names: 则是配置缓存名称,多个名称可以使用逗号分隔,以便于缓存注解的引用。
  • 为了使用缓存管理器,需要在 Spring Boot 启动类 中加入驱动缓存的注解 @EnableCaching
@SpringBootApplication
@EnableCaching  // 使用注解缓存驱动机制
public class SpringbootDataMybatis01Application {
   
    // ...
    public static void main(String[] args) {
        SpringApplication.run(SpringbootDataMybatis01Application.class, args);
    }
}

4.2 开发缓存注解

配置

# 数据库配置
spring:
  datasource:
    username: root
    password: admin
    url: jdbc:mysql:///eesy?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    # 配置redis连接池
    jedis:
      pool:
        min-idle: 5
        max-active: 10
        max-idle: 10
        max-wait: 2000
    # redis服务器属性
    port: 6379
    host: 127.0.0.1
    # password: 123456
    # redis 连接池超时时间,单位毫秒
    timeout: 1000
  # 缓存配置
  cache:
    type: redis
    cache-names: redisCache
    redis:
      use-key-prefix: false    # 禁用前缀
      cache-null-values: true  # 允许缓存空值
      # key-prefix:  自定义前缀
      time-to-live: 600000 # 定义超时时间,单位毫秒  10分钟=600000ms

# 整合mybatis
mybatis:
  # 定义别名扫描的包
  type-aliases-package: top.roise.domain
  mapper-locations: classpath:mybatis/mapper/*.xml
  # mybatis 外部配置文件
  # config-location:

  # 级联延迟加载属性配置
  # configuration:
  # aggressive-lazy-loading: false

properties

logging.level.root=debug
logging.level.org.springframework=debug
logging.level.org.mybatis=debug


# 配置servlet容器相关
server.port=8080
server.servlet.context-path=/boot

# 配置tomcat相关
server.tomcat.uri-encoding=utf-8
server.tomcat.connection-timeout=5000

# 解决:@DeleteMapping请求时出错,报Request method 'POST' not supported错误
spring.mvc.hiddenmethod.filter.enabled=true

mybatis用户操作接口

// 表明这是一个mybatis的mapper类
@Mapper
@Repository
public interface UserMapper {

    List<User> findUsers(@Param("username") String username, @Param("address") String address);

    User getUser(Long id);

    int insertUser(User user);

    int updateUser(User user);

    int deleteUser(Long id);
}

用户服务接口

public interface UserService {

    User insertUser2(User user);

    User getUser(Long id);

    User updateUserName(Long id, String username);

    List<User> findUsers(String username,String address);

    int deleteUser(Long id);

}

用户服务实现类,使用 Spring缓存注解

Service
public class UserServieImpl implements UserService , ApplicationContextAware {

    @Autowired
    private UserMapper userMapper;

    private ApplicationContext applicationContext;

    // 实现生命周期方法,设置IOC容器
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Transactional
    @CachePut(value = "redisCache",key = "'redis_user_'+#result.id")
    @Override
    public User insertUser2(User user) {
        userMapper.insertUser(user);
        return user;
    }

    @Cacheable(value = "redisCache",key = "'redis_user_'+#id")
    @Override
    public User getUser(Long id) {
        return userMapper.getUser(id);
    }

    @Transactional
    @CachePut(value = "redisCache",condition = "#result != 'null'",key = "'redis_user_'+#id")
    @Override
    public User updateUserName(Long id, String username) {
        User user = this.getUser(id);
        if(Objects.isNull(user)){
            return null;
        }

        user.setUsername(username);
        userMapper.updateUser(user);
        return user;
    }

    // 命中率低,不采用缓存机制
    @Override
    public List<User> findUsers(String username, String address) {
        return userMapper.findUsers(username,address);
    }

    // 方法执行后,移除缓存
    @Override
    @CacheEvict(value = "redisCache",key="'redis_user_'+#id",beforeInvocation = false)
    public int deleteUser(Long id) {
        return userMapper.deleteUser(id);
    }
}

使用的注解:

注解 作用
@CachePut 表示将 方法结果存回到缓存中
@Cacheable 表示先从缓存中定义的键查询,查到就返回,否则查询数据库,然后把返回结果存到缓存中
@CacheEvict 通过定义的键移除缓存,它有一个Boolean类型的配置项 beforeInvocation,表示在方法之前或者之后移除缓存。因为默认值为 false,表示方法之后移除缓存

参数解释:

  • #id :代表参数,通过参数名称匹配,要求方法参数有一个名为 id的参数
  • #result :代表返回的结果对象,像上面就代表 User对象,#result.id 就是取出它的属性 id

@CachePut 使用了属性 condition配置项,可使用spel表达式,返回true,表示使用缓存操作。否则不缓存

注意:Redis缓存机制,会使用 #{cacheName}:#{key} 的形式作为键保存数据,像上面的键就是:“redisCache:redis_user_1

4.3 缓存注解自调用失效问题

updateUserName方法调用 getUser,属于类自调用,并不存在代理对象的调用,就没有使用 aop。如何解决,

  • 2个Service
  • xxxAware,从ioc容器获取代理对象

4.4 自定义缓存管理器

配置注解缓存 redis,消除前缀,设置超时时间

# 禁用前缀
spring.cache.redis.use-key-prefix=false
# 允许保存空值
# spring.cache.redis.cache-null-values=true
# 自定义前缀
# spring.cache.redis.key-prefix=
# 定义超时时间,单位毫秒  10分钟
spring.cache.redis.time-to-1ive=600000

然后上面的键就变成了,“redis_user_1"。

或者使用 自定义缓存管理器

    @Autowired
    private RedisConnectionFactory connectionFactory;
    
    // 自定义 Redis 缓存管理器
    @Bean(name = "redisCacheManager")
    public RedisCacheManager initRedisCacheManager(){
        // Redis加锁的写入器
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
        // 启动Redis缓存的默认设置
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // 设置JDK序列化器
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
        // 禁用前缀
        config = config.disableKeyPrefix();
        // 设置 10min 超时
        config = config.entryTtl(Duration.ofMinutes(10));
        // 创建 redis缓存管理器
        RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
        return redisCacheManager;
    }

你可能感兴趣的:(SpringBoot,java,redis,springboot)