Redis学习3——Redis应用之缓存

引言

缓存的意义

Redis作为一个NoSql数据库,被广泛的当作缓存数据库使用,所谓缓存,就是数据交换的缓冲区。使用缓存的具体原因有:

  1. 缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,通过缓存,可以大大降低用户访问并发量带来的服务器读写压力。
  2. 实际开发过程中,企业的数据量,少则几十万,多则上千万,如此大的数据量,如果没有缓存来作为“避震器”,系统是几乎撑不住的,所以企业会大量运用缓存技术。

image.png

缓存的分类

对于缓存,我们可以分为以下几类:

  1. 浏览器缓存:主要存在于浏览器端的缓存
  2. 应用层缓存:本地缓存(比如tomcat本地缓存)或缓存数据库(比如redis)
  3. 数据库缓存:在数据库中有一片空间是buffer pool,增删改查数据都会先加载到mysql的缓存中
  4. CPU缓存:当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存

缓存模型

查询、添加缓存

一般情况下,在查询数据库之前,先查询缓存,如果缓存数据存在,就直接返回缓存数据,如果不存在,查询数据库,并将数据存入redis,具体的模型如下:
image.png

缓存淘汰

内存的空间是有限的,当我们向redis存储太多数据时,redis会对部分数据进行淘汰,淘汰数据的场景有:

  1. 内存淘汰:Redis自动进行,当redis内存达到设定的max-memery时,会自动触发淘汰机制,淘汰掉一些不重要的数据。
  2. 超时剔除:当对redis设置了过期时间TTL时,Redis会将超时的数据进行删除。
  3. 主动更新:通过手动调用方法把缓存删除掉,常用于解决缓存与数据库不一致的问题。

Redis自动进行内存淘汰时,主要有6+2种数据淘汰机制,可以分为以下三大类:

  1. 不淘汰数据
  • noeviction ,不进行数据淘汰,当缓存被写满后,Redis不提供服务直接返回错误。
  1. 在设置过期时间的键值对中,
  • volatile-random ,在设置过期时间的键值对中随机删除
  • volatile-ttl ,在设置过期时间的键值对,基于过期时间的先后进行删除,越早过期的越先被删除。
  • volatile-lru , 基于LRU(Least Recently Used) 算法筛选设置了过期时间的键值对, 最近最少使用的原则来筛选数据
  • volatile-lfu ,使用 LFU( Least Frequently Used ) 算法选择设置了过期时间的键值对, 使用频率最少的键值对,来筛选数据。
  1. 在所有键值对中
  • allkeys-random, 从所有键值对中随机选择并删除数据
  • allkeys-lru, 使用 LRU 算法在所有数据中进行筛选
  • allkeys-lfu, 使用 LFU 算法在所有数据中进行筛选

image.png

数据库缓存不一致解决方案

因为我们的缓存数据来自于数据库,而数据库的数据会随业务的需要而发生变化,当数据库中的数据变化,而缓存没有同步该数据,此时便会产生一致性问题。具体的解决策略有:

  1. Cache Aside Pattern(旁路缓存)
  2. Read/Write-Through Pattern(读写穿透)
  3. Write Behind Pattern(异步缓存写入)
Cache Aside Pattern(旁路缓存模式)

该模式是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景,在该模式中,需要同时维系db和cache,并且以db的结果为准。

先更新db,然后再直接删除cache
image.png
注意,不能先删除cache,在写db,因为在并发的情况下,可能存在,请求1删除缓存,然后写db,并且写db操作还没结束,这时请求2获取缓存,此时因为缓存被删除了,因此会从数据库读数据, 并存入缓存,这个时候的数据是未更新的,当请求2结束后,请求1写db结束,此时就会造成数据库和缓存中的数据不一致。
image.png
旁路缓存理论上来说,也会造成数据库和缓存不一致的问题,比如:请求 1 先写数据 A,请求 2 随后读数据 A 的话,就很有可能产生数据不一致性的问题。
这个过程可以简单描述为:
image.png
但这种概率很小,因为缓存的写入速度比数据库的写入速度快很多。

从cache中读取数据,读取到就直接返回。如果读取不到,从db中读取数据返回,然后再把数据放到cache中。
image.png

Cache Aside Pattern的缺陷
  1. 首次请求数据一定不在cache:这种情况可以先将热点数据提前放入cache中。
  2. 写操作比较频繁,会导致cache的数据被频繁删除,影响缓存命中率:更新db的时候同时更新cache,但是给缓存设置一个比较短的过期时间,保证即使数据不一致的话,影响也比较小。
Read/Write Through Pattern(读写穿透)

服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 db,从而减轻了应用程序的职责。这种策略比较少使用,因为Redis并没有提供cache将数据写入db的功能。

写(Write Through)
  • 先查 cache,cache 中不存在,直接更新 db。
  • cache 中存在,则先更新 cache,然后 cache 服务自己更新 db(同步更新 cache 和 db)。

读(Read Through)
  • 从 cache 中读取数据,读取到就直接返回 。
  • 读取不到的话,先从 db 加载,写入到 cache 后返回响应。

缺陷
  1. 实现困难,如Redis并没有提供cache将数据写入db的功能
  2. 和Cache Aside Pattern一样,首次请求数据一定不在cache中
Write Behind Pattern(异步缓存写入)

与Read/Write Through Pattern相似,都是由cache服务来负责cache和db的读写,但是,Read/Write Through是同步更新cache和db,而Write Behind是只更新缓存,不直接更新db,改为异步批量的方式,更新db。
这种方式对数据一致性带来了更大的挑战,比如 cache 数据可能还没异步更新 db 的话,cache 服务可能就就挂掉了。这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 Innodb Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 db 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

缓存常见的问题

缓存雪崩

缓存雪崩,指缓存在同一时间大面积的生效(比如缓存数据过期),导致大量的请求都直接落到数据库上,给数据库造成了巨大的压力。此外,如果缓存服务器宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。

解决方法有:

  1. 采用Redis集群,避免单机出现问题导致整个缓存服务器无法使用。
  2. 限流,避免同时处理大量请求。
  3. 多级缓存,比如本地缓存+Redis缓存的组合,当Redis缓存出现问题时,可以从本地缓存中获取部分数据。
  4. 设置不同的生效时间比如随机设置缓存的失效时间。
  5. 缓存永不生效。
  6. 缓存预热,在程序启动后或运行过程中,主动将热点数据加载到缓存中。
缓存穿透

缓存穿透指大量请求的key是不合理的,根本不存在与缓存和数据库中,这就导致这些请求直接到了数据库,给数据库造成巨大压力。比如黑客故意制造一些非法的key发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。

解决方法:

  1. 参数校验:对不合法的参数请求,直接抛出异常信息给客户端,比如查询的数据库id不能为0,手机格式不正确等。
  2. 缓存无效key:将这些无效的key设置到缓存中,并设置一个较短的过期时间。这种方法可以解决请求的key变化不频繁的情况。
  3. 接口限流。
  4. 布隆过滤器:通过布隆过滤器判断一个给定数据是否存在于海量数据中。具体做法是:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

缓存击穿

缓存击穿,指请求的key对应的是热点数据,该数据存在于数据库中,但是不存在于缓存中(通常是因为缓存中的数据已经过期),这就会导致瞬时大量请求直接打到了数据库上,对数据库造成巨大压力。比如秒杀进行过程中,缓存中的某个秒杀商品数据过期,导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成巨大压力。

解决方法:

  1. 设置热点数据永不过期或过期时间较长。
  2. 针对热点数据提取预热,将其存入缓存中并设置合理的过期时间。
  3. 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。

SpringBoot整合Redis实现缓存

准备数据库数据

首先我们创建一个t_user表,并插入几条数据,用于后续的查询:
image.png

读缓存

创建User类和对应的UserMapper

@TableName(value = "t_user")
@Data
public class User implements Serializable {
    @TableId(type = IdType.AUTO)
    private Integer id;

    private String username;

    private String password;
}

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

UserService

@Service
public class UserService {
    // 过期时间
    private long ttl = 100L;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RedisUtils redisUtils;

    public User getUserById(Integer id) {
        String key = "user:" + id;
        Object value = redisUtils.getKey(key);
        if (value != null) {
            // 缓存中存在该数据
            return (User) value;
        }

        // 缓存中不存在该数据, 从数据库中查询
        User userFromDB = getUserFromDB(id);
        if (userFromDB == null) {
            return null;
        }
        redisUtils.addKey(key, userFromDB, ttl);
        return userFromDB;
    }

    private User getUserFromDB(Integer id) {
        System.out.println("从数据库中查询====================");
        return userMapper.selectById(id);
    }
}

添加一个测试方法:

package org.example;

import org.example.pojo.User;
import org.example.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
public class RedisCacheTest {
    @Autowired
    private UserService userService;

    @Test
    public void testGetUserById() {
        User userById = userService.getUserById(1);
        System.out.println(userById);

        userById = userService.getUserById(1);
        System.out.println(userById);
    }
}

执行测试方法,结果如下:
image.png
从上述内容中,可以看出,第一次查询时,因为数据不存在于缓存中,所以会查询数据库,第二次查询时,从缓存中查询到数据,直接返回。
但是这里涉及到一个序列化的问题,我们通过下载一些redis桌面可视化工具,查看这些数据,发现这些key和value都有一些奇怪的编码,如下图所示:
image.png
这是因为,默认情况下,redisTemplate使用JDK的序列化器(JdkSerializationRedisSerializer)对存储的对象进行序列哈和反序列化操作,这可能导致序列化结果不可读或占用较多的存储空间。因此,我们需要在配置redisTemplate的时候,设置对应的序列化器,我们修改上一章的RedisConfiguration类,修改结果如下:

package org.example.config;

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;

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置key的序列化器为StringRedisSerializer
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // 设置value的序列化器为Jackson2JsonRedisSerializer
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        // 设置hash key的序列化器为StringRedisSerializer
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 设置hash value的序列化器为Jackson2JsonRedisSerializer
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 完成其他配置后,初始化RedisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

我们将之前那些key全部删除掉,然后重新运行测试代码,结果如下:
image.png
通过redis可视化工具,也能清楚地看到每个key和value的结构和信息。
image.png
上面的代码,存在一些问题,当请求1查询id=1的用户,从缓存中查询不到时,会查询数据库,然后将数据更新到数据库,然后请求2查询id=1的用户,从缓存中查询到,便直接返回,这是理想的情况。但是,可能请求1查询数据库还没执行完,请求2过来了,此时缓存中没有id=1的用户,因此请求2也会去查询数据库。
比如我们修改getUserFromDB,加上Thread.sleep(2000),模拟数据库的查询较慢,然后修改测试代码,模拟多个请求同时查询id=1的用户的情况:

   @Test
    public void testGetUserById() throws ExecutionException, InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        Callable<User> getUserTask = () -> userService.getUserById(1);

        Future<User> submit1 = executorService.submit(getUserTask);
        Future<User> submit2 = executorService.submit(getUserTask);
        System.out.println(submit1.get());
        System.out.println(submit2.get());
    }

此时再次执行测试代码,结果如下:
image.png
解决上面的情况,方式就是加锁,我们修改UserService的代码如下,这里通过synchronized加锁,然后锁的对象使用key,这样可以将锁的粒度缩小一点。

 public User getUserById(Integer id) {
        String key = "user:" + id;
        Object value = redisUtils.getKey(key);
        if (value != null) {
            // 缓存中存在该数据
            return (User) value;
        }

        User userFromDB = null;
        synchronized (key.intern()) {
            // 再次查询缓存
            value = redisUtils.getKey(key);
            if (value != null) {
                return (User) value;
            }

            // 缓存中不存在该数据, 从数据库中查询
            userFromDB = getUserFromDB(id);
            if (userFromDB == null) {
                return null;
            }
            redisUtils.addKey(key, userFromDB, ttl);
        }
        return userFromDB;
    }

    private User getUserFromDB(Integer id) {
        try {
            Thread.sleep(2000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("从数据库中查询====================");
        return userMapper.selectById(id);
    }

重新运行测试方法,结果如下:
image.png
通过加锁,可以解决被访问多次的情况,但是这样的锁有一个问题,这个锁,只适用于单机的情况,当我们的服务部署在多台服务器上时,这些服务器上的应用,对应的JVM都不同,因此锁住的,不是同一个对象,后续会单独对分布式锁进行讲解,这里先不予讨论。

更新缓存

修改userService,添加修改用户的方法

 public void updateUser(UpdateUserRequest updateUserRequest) {
        if (updateUserRequest.getId() == null) {
            return;
        }
        User user = getUserById(updateUserRequest.getId());
        if (user == null) {
            return;
        }
        if (StringUtils.isNotEmpty(updateUserRequest.getUsername())) {
            user.setUsername(updateUserRequest.getUsername());
        }
        if (StringUtils.isNotEmpty(updateUserRequest.getPassword())) {
            user.setPassword(updateUserRequest.getPassword());
        }
        // 先更新数据库
        int updateResult = userMapper.updateById(user);
        if (updateResult == 1) {
            // 更新成功,删除缓存
            redisUtils.removeKey("user:" + updateUserRequest.getId());
        }
    }

添加测试方法

 @Test
    public void testUpdateUser() {
        UpdateUserRequest updateUserRequest = new UpdateUserRequest();
        updateUserRequest.setId(1);
        updateUserRequest.setPassword("12345");
        userService.updateUser(updateUserRequest);

        User userById = userService.getUserById(1);
        System.out.println(userById);
    }

测试结果如下
image.png

SpringBoot整合SpringCache+Redis

在上面的代码中,我们发现,和Redis查询的操作,和我们的业务代码糅合在一起, 不方便进行处理,如果后续我们缓存服务器从Redis改为其他缓存服务器,需要修改多处代码,不利于项目的扩展和维护。
SpringCache框架是Spring框架提供的一套基于注解的缓存解决方案,在应用程序中简化了缓存操作的管理和使用。它通过在方法上添加缓存注解,来实现自动缓存的功能,其常用注解有:
1)@Cacheable: 表示该方法返回结果可以被缓存,当方法被调用时,先检查缓存,如果缓存命中,直接返回缓存中的结果,不再执行方法体中的逻辑。
2) @CachePut:表示该方法的返回结果需要更新缓存,每次方法被调用后,都会将返回结果更新到缓存中。
3)@CacheEvit:表示该方法会清除缓存中的数据,可以用于在更新或删除数据时清除相应的缓存。

整合SpringCache

添加Spring Cache的依赖

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

在启动类上加上注解开启缓存

@SpringBootApplication
@EnableCaching
public class RedisApp {
    public static void main(String[] args) {
        SpringApplication.run(RedisApp.class, args);
    }
}

测试Cacheable和CacheEvict

我们在userService类上,加上两个方法,用于测试Cacheable和CacheEvit注解。
添加getUserById2方法,该方法中的逻辑为查询数据库操作

  @Cacheable(value = "user", key = "#id")
    public User getUserById2(Integer id) {
        System.out.println("从数据库中查询==============");
        return userMapper.selectById(id);
    }

然后添加一个测试方法

 @Autowired
    private UserService userService;

    @Test
    public void testGetUserById() {
        System.out.println(userService.getUserById2(1));
        System.out.println(userService.getUserById2(1));
    }

测试结果如下,第一次查询时,查询数据库,第二次直接查询缓存。
image.png
然后我们查看reids缓存,发现这个key的值是user::1,这个和我们之前定义的不太相同,而且没有过期时间,除此之外,这个value的值,序列化有问题
image.png
我们修改RedisConfiguration,添加RedisCacheConfiguration的相关配置

package org.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置key的序列化器为StringRedisSerializer
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // 设置value的序列化器为Jackson2JsonRedisSerializer
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        // 设置hash key的序列化器为StringRedisSerializer
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 设置hash value的序列化器为Jackson2JsonRedisSerializer
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 完成其他配置后,初始化RedisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(){
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        //相当于new了一个RedisCacheConfiguration
        RedisCacheConfiguration  configuration = RedisCacheConfiguration.defaultCacheConfig();
        configuration = configuration.serializeValuesWith
                // 指定value序列化器
                        (RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                // 指定 key的TTL
                .entryTtl(Duration.ofSeconds(100))
                // 指定前缀
                .prefixCacheNameWith("user::");
        return configuration;
    }
}

删除原先的缓存,然后重新运行测试代码,然后查看缓存信息,结果如下:
image.png
此时过期时间和序列化均设置成功。但这样配置还有一些问题,通过这种全局配置,所有cache key的TTL都一样,实际开发中,不同的cache key可能需要的TTL都不同,我们修改RedisConfiguration,为不同的key配置上不同的过期时间

package org.example.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.cache.RedisCacheManagerBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class RedisConfiguration {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置key的序列化器为StringRedisSerializer
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // 设置value的序列化器为Jackson2JsonRedisSerializer
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);

        // 设置hash key的序列化器为StringRedisSerializer
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 设置hash value的序列化器为Jackson2JsonRedisSerializer
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

        // 完成其他配置后,初始化RedisTemplate
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    @Bean
    public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper().enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        return (builder) -> {
            Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();

            cacheConfigurations.put("user",
                    RedisCacheConfiguration.defaultCacheConfig()
                            .entryTtl(Duration.ofSeconds(60))
                            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)));

            cacheConfigurations.put("product",
                    RedisCacheConfiguration.defaultCacheConfig()
                            .entryTtl(Duration.ofSeconds(120))
                            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)));

            builder.withInitialCacheConfigurations(cacheConfigurations);
        };
    }
    
}

运行结果如下:
image.png
接着,我们测试CacheEvict,首先在userService中添加一个更新方法

  @CacheEvict(value = "user", key = "#updateUserRequest.id")
    public void updateUser2(UpdateUserRequest updateUserRequest) {
        if (updateUserRequest.getId() == null) {
            return;
        }
        User user = getUserById2(updateUserRequest.getId());
        if (user == null) {
            return;
        }
        if (StringUtils.isNotEmpty(updateUserRequest.getUsername())) {
            user.setUsername(updateUserRequest.getUsername());
        }
        if (StringUtils.isNotEmpty(updateUserRequest.getPassword())) {
            user.setPassword(updateUserRequest.getPassword());
        }
        // 先更新数据库
        userMapper.updateById(user);
    }

然后添加测试方法:

 @Test
    public void testUpdateUser() {
        UpdateUserRequest updateUserRequest = new UpdateUserRequest();
        updateUserRequest.setId(1);
        updateUserRequest.setPassword("123456");
        userService.updateUser2(updateUserRequest);
    }

运行该测试方法,然后去看对应的redis缓存,发现更新成功后,该缓存被删除掉,说明CacheEvict生效
image.png

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/597207.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

前后端功能实现——查询所有

目录 1、需求 2、步骤 1&#xff09;创建模块 引入坐标 2&#xff09;创建结构 实现三层架构 3&#xff09;创建表 brand 4&#xff09;创建实体类 Brand 5&#xff09;创建MyBatis配置文件 6&#xff09;创建映射文件 7&#xff09;创建工具类 SqlSessionFactoryUti…

基于FPGA的数字信号处理(9)--定点数据的两种溢出处理模式:饱和(Saturate)和绕回(Wrap)

1、前言 在逻辑设计中&#xff0c;为了保证运算结果的正确性&#xff0c;常常需要对结果的位宽进行扩展。比如2个3bits的无符号数相加&#xff0c;只有将结果设定为4bits&#xff0c;才能保证结果一定是正确的。不然&#xff0c;某些情况如77 14(1110)&#xff0c;如果结果只…

部署YUM仓库以及NFS共享服务

YUM仓库部署 一.YUM概述 YUM仓库源是一种软件包管理工具&#xff0c;用于在Linux系统上安装、更新和删除软件包。YUM仓库源包含了软件包的元数据信息和实际的软件包文件。用户可以通过配置YUM仓库源&#xff0c;从中下载和安装软件包。 常见的YUM仓库源包括&#xff1a; 本…

【一起深度吧!】24/05/03

池化层 最大池化和平均层化&#xff1a;最大池化&#xff1a;平均池化&#xff1a; 从零实现池化层&#xff1a; 最大池化和平均层化&#xff1a; 池化的作用: 1、可以降维&#xff0c;减少要 训练的参数。 2、提取特征&#xff0c;也就是保留主要的特征&#xff0c;过滤掉不重…

7-zip下载、安装

7-Zip 官方中文网站 (sparanoid.com) 7-Zip - 程序下载 (sparanoid.com)

Unity 性能优化之图片优化(八)

提示&#xff1a;仅供参考&#xff0c;有误之处&#xff0c;麻烦大佬指出&#xff0c;不胜感激&#xff01; 文章目录 前言一、可以提前和美术商量的事1.避免内存浪费&#xff08;UI图片&#xff0c;不是贴图&#xff09;2.提升图片性能 二、图片优化1.图片Max Size修改&#x…

Eayswoole 报错 crontab info is abnormal

在执行一个指定的定时任务时 如 php easyswoole crontab show 报错 crontab info is abnormal 如下图所示&#xff1a; 查询了半天 修改了如下配置&#xff1a; 旧的 // 创建定时任务实例 $crontab new \EasySwoole\Crontab\Crontab($crontabConfig); 修改后&#…

山海鲸医疗科技:引领智慧医疗新潮流

随着科技的飞速发展&#xff0c;智慧医疗已经成为医疗行业创新的重要方向。在这个背景下&#xff0c;山海鲸智慧医疗解决方案应运而生&#xff0c;以其先进的技术和全面的服务&#xff0c;为医疗行业带来了前所未有的变革。 山海鲸智慧医疗解决方案是一套集成医疗信息化、大数…

【OneAPI】网页截图API

OneAPI新接口发布&#xff1a;网页截图 可生成指定URL的网页截图。 接口地址&#xff1a;https://oneapi.coderbox.cn/openapi/api/webpage/screenshot 请求参数 参数名类型必填含义说明urlstring是要截图的网页链接例如&#xff1a;https://baidu.comwidthnumber否截图宽度…

龙迅LT9211D MIPI桥接到2 PORT LVDS,分辨率支持高达3840*2160*30HZ

龙迅LT9211D描述&#xff1a; Lontium LT9211D是一款高性能的MIPI DSI/CSI- 2到双端口LVDS转换器。LT9211D反序列化输入的MIPI视频数据&#xff0c;解码数据包&#xff0c;并将格式化的视频数据流转换为AP和移动显示面板或摄像机之间的LVDS发射机输出。LT9211D支持最大14 dB输…

手机运营商二要素验证接口:确保业务操作安全可靠

手机运营商二要素验证接口是一种通过与电信运营商合作的方式&#xff0c;检验手机用户的手机号码与姓名是否一致的服务。这个接口可以广泛应用于各种需要用户实名认证的场景&#xff0c;例如电商、游戏、直播以及金融等行业。 这个接口的作用非常重要&#xff0c;它可以帮助企…

C++——list的特性及使用

list的特性 STL中的list是指带头双向循环列表&#xff0c;list每个元素的存储相互独立&#xff0c;因为其节点存储位置独立不连续&#xff0c;其插入和删除不需要挪动其他元素效率比起vector更高。但也因为存储空间不连续的问题&#xff0c;不能做到和vector一样的随机…

鸿蒙编译子系统详解(二)main.py

1.5.4源码解析 1.5.4.1 build/hb/main.py脚本 这个脚本是编译的主程序脚本&#xff0c;流程如下&#xff1a; 首先是初始化各种module类&#xff0c;然后运行对应模块。 hb分为build,set,env,clean,tool,help几个模块&#xff0c;模块源码位于build/hb/modules/目录下&#xff…

学生管理系统初级

根据题目要求生成大纲 总结: 1.在书写时&#xff0c;考虑到了书写时id可是是abc... 类型是String&#xff0c;但在根据id获取集合中元素时 list.get() &#xff0c;get&#xff08;&#xff09;里面是int类型。 2.在书写还有一点功能并不完全&#xff0c; 2.1查找时是打印所有…

【NodeMCU实时天气时钟温湿度项目 1】连接点亮SPI-TFT屏幕和UI布局设计

前言 从今天开始&#xff0c;我们详解介绍制作实时天气时钟项目的方法步骤&#xff0c;主要分以下几个专题分别进行&#xff1a;&#xff08;1&#xff09;连接点亮SPI-TFT屏幕和UI布局设计&#xff1b;&#xff08;2&#xff09;NodeMCU的WIFI模式设置及连接&#xff1b;&…

车牌号识别系统:PyQT5+QT Designe+crnn/PaddleOCR+YOLO+OpenCV矫正算法。

PyQT5&QT Designecrnn/PaddleOCRYOLO传统OpenCV矫正算法。可视化的车牌识别系统项目。 车牌号识别系统 项目绪论1.项目展示2.视频展示3.整体思路 一、PyQT5 和 QT Designer1.简介2.安装3.使用 二、YOLO检测算法三、OpenCV矫正算法四、crnn/PaddleOCR字符识别算法五、QT界面…

FreeRTOS任务详解

一、任务的创建与删除 1.任务的基本概念 RTOS系统的核心就是任务管理,FreeRTOS 也不例外,而且大多数学习 RTOS 系统的工程 师或者学生主要就是为了使用 RTOS 的多任务处理功能,初步上手 RTOS 系统首先必须掌握的 也是任务的创建、删除、挂起和恢复等操作,由此可见任务管理…

限量背包问题

问题描述 限量背包问题&#xff1a;从m个物品中挑选出最多v个物品放入容量为n的背包。 问题分析 限量背包问题&#xff0c;可以用来解决许多问题&#xff0c;例如要求从n个物品中挑选出最多v个物品放入容量为m的背包使得背包最后的价值最大&#xff0c;或者总共有多少种放法…

我独自升级:崛起怎么下载 我独自升级游戏下载教程分享

定于5月8日全球揭幕的《我独自升级崛起》——一款扣人心弦的动作RPG巨制&#xff0c;灵感采撷于同名动画及网络漫画的热潮&#xff0c;誓将引领满怀热忱的玩家步入一场交织着深邃探索和宏大规模的奇妙冒险。该游戏立足于一个独树一帜的网络武侠宇宙&#xff0c;细腻刻画了一个凡…

VSCode通过SSH连接虚拟机Ubuntu失败

问题说明 最近使用VSCode通过SSH连接Ubuntu&#xff0c;通过VSCode访问Ubuntu进行项目开发&#xff0c;发现连接失败 在VSCode中进行SSH配置 这些都没有问题&#xff0c;但在进行连接时候出现了问题&#xff0c;如下&#xff1a; 出现了下面这个弹窗 解决方法 发现当…