在 Spring Boot 中使用 Redis 布满荆棘的人生 2022-10-01 08:54 157阅读 0赞 ## Redis 本身的一些概念 ## 本文中的代码见 [spring-boot-redis][]。 ### Redis 支持的数据结构 ### * String 字符串 * Hash 字典 * List 列表 * Set 集合 * Sorted Set 有序集合 ### String 和 Hash 的对比 ### String 实际是就是一个 `Key - Value` 的映射; Hash 就是一个 `Key - (Key - Value)` 的两层映射。 # redis-cli # Redis 中命令不区分大小写。这里命令使用小写,仅在特别的地方用大写。 # 参数使用“大写+下划线”的方式。 # String set KEY VALUE get KEY # Hash hset HASH_NAME KEY VALUE hget HASH_NAME KEY hMset HASH_NAME KEY0 VALUE0 KEY1 VALUE1 ... hMget HASH_NAME KEY0 KEY1 ... 复制代码 [STACK OVERFLOW 上一个对 String 和 Hash 的讨论][STACK OVERFLOW _ String _ Hash] 对于一个对象是把本身的数据序列化后用 String 存储,还是使用 Hash 来分别存储对象的各个属性: * 如果在大多数时候要访问对象的大部分数据:使用 String * 如果在大多数时候只要访问对象的小部分数据:使用 Hash * 如果对象里面还有对象这种结构复杂的,最好用 String。否则最外层用 Hash,里面又将对象序列化,两者混用可能导致混乱。 ## Spring Boot 添加 Redis 的配置 ## > 以 gradle 为例。 * 修改 `build.gradle` compile("org.springframework.boot:spring-boot-starter-data-redis") 复制代码 * 修改 `application.yml` spring: # redis redis: host: 127.0.0.1 # 数据库索引(默认为0) database: 0 port: 6379 password: PASSWORD # 连接池中的最大空闲连接 pool.max-idle: 8 # 连接池中的最小空闲连接 pool.min-idle: 0 # 连接池最大连接数(使用负值表示没有限制) pool.max-active: 8 # 连接池最大阻塞等待时间(使用负值表示没有限制) pool.max-wait: -1 # 连接超时时间(毫秒) timeout: 0 复制代码 * 添加 `RedisConfig` package zz.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; /** * RedisConfig * * @author zz * @date 2018/5/7 */ @Configuration @EnableCaching @Slf4j public class RedisConfig extends CachingConfigurerSupport { @Bean public KeyGenerator wiselyKeyGenerator() { return new KeyGenerator() { private static final String SEPARATE = ":"; @Override public Object generate(Object target, Method method, Object... params) { log.debug("+++++generate"); StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(SEPARATE).append(method); for (Object obj : params) { sb.append(SEPARATE).append(obj); } return sb.toString(); } }; } /** * https://www.jianshu.com/p/9255b2484818 * * TODO: 对 Spring @CacheXXX 注解进行扩展:注解失效时间 + 主动刷新缓存 */ @Bean public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) { log.debug("++++cacheManager"); RedisCacheManager redisCacheManager =new RedisCacheManager(redisTemplate); redisCacheManager.setTransactionAware(true); redisCacheManager.setLoadRemoteCachesOnStartup(true); // 最终在 Redis 中的 key = @Cacheable 注解中 'cacheNames' + 'key' redisCacheManager.setUsePrefix(true); // 所有 key 的默认过期时间,不设置则永不过期 // redisCacheManager.setDefaultExpiration(6000L); // 对某些 key 单独设置过期时间 // 这里的 key 是 @Cacheable 注解中的 'cacheNames' Map<String, Long> expires = new HashMap<>(10); // expires.put("feedCategoryDto", 5000L); // expires.put("feedDto", 5000L); redisCacheManager.setExpires(expires); return redisCacheManager; } // value serializer private Jackson2JsonRedisSerializer getJackson2JsonRedisSerializer() { Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); return jackson2JsonRedisSerializer; } private GenericJackson2JsonRedisSerializer getGenericJackson2JsonRedisSerializer() { return new GenericJackson2JsonRedisSerializer(); } /** * * Once configured, the template is thread-safe and can be reused across multiple instances. * -- https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/ */ @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { log.debug("++++redisTemplate"); StringRedisTemplate template = new StringRedisTemplate(factory); // key serializer StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); RedisSerializer valueRedisSerializer; // -- 1 Jackson2JsonRedisSerializer // valueRedisSerializer = getJackson2JsonRedisSerializer(); // -- 2 GenericJackson2JsonRedisSerializer valueRedisSerializer = getGenericJackson2JsonRedisSerializer(); // set serializer template.setKeySerializer(stringRedisSerializer); template.setValueSerializer(valueRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); template.setHashValueSerializer(valueRedisSerializer); template.afterPropertiesSet(); return template; } } 复制代码 RedisConfig 中定义了三个函数,主要作用如下: * wiselyKeyGenerator:定义了一个生成 Redis 的 key 的方法。如下文使用了 @Cacheable 注解的地方,可以指定 key 的生成方法使用我们这个函数。 * cacheManager:定义了对 Redis 的一些基本设置。 * redisTemplate:对我们要使用的 RedisTemplate 做一些设置。主要是确定序列化方法。 ### RedisTemplate 设置序列化器 ### Spring Redis 虽然提供了对 list、set、hash 等数据类型的支持,但是没有提供对 POJO 对象的支持,底层都是把对象序列化后再以字节的方式存储的。 ##### 因此,Spring Data Redis 提供了若干个 Serializer,主要包括: ##### * JdkSerializationRedisSerializer: 默认的序列化器。序列化速度快,生成的字节长度较大。 * OxmSerializer: 生成 XML 格式的字节。 * StringSerializer: 只能对 String 类型进行序列化。 * JacksonJsonRedisSerializer:以 JSON 格式进行序列化。 * Jackson2JsonRedisSerializer:JacksonJsonRedisSerializer 的升级版。 * GenericJackson2JsonRedisSerializer:Jackson2JsonRedisSerializer 的泛型版。 ##### RedisTemplate 中需要声明 4 种 serializer(默认使用的是 `JdkSerializationRedisSerializer`): ##### * keySerializer :对于普通 K-V 操作时,key 采取的序列化策略 * valueSerializer:value 采取的序列化策略 * hashKeySerializer: 在 hash 数据结构中,hash-key 的序列化策略 * hashValueSerializer:hash-value 的序列化策略 无论如何,建议 key/hashKey 采用 StringRedisSerializer。 > by [Spring-data-redis: serializer实例][Spring-data-redis_ serializer] **我们设置了 serializer 后,读写 Redis 要使用同一种 serizlizer,否则会读不出之前用不同 serializer 写入的数据。** 也就是设置 valueSerializer 为GenericJackson2JsonRedisSerializer,然后写入了数据。 后面要读数据的时候,如果将 valueSerializer 又设置成了 Jackson2JsonRedisSerializer,那么读取数据时就会报错。 > 通常情况下,我们只需要在 RedisConfig 中统一设置好 4 个 serializer 即可。 #### Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 的对比 #### * 两者都是将对象的数据序列化成 JSON 格式的字符串。 * Jackson2JsonRedisSerializer 需要自己指定 ObjectMaper 或某个特定的类型。 * GenericJackson2JsonRedisSerializer 是 Jackson2JsonRedisSerializer 的一个特例,默认支持所有类型。 * 两者序列化时,都会将原始对象的类名和包名写入 JSON 字符串中。以便反序列化时,确认要将 JSON 转成何种格式。 ##### 可用如下方式来获得通用的 Jackson2JsonRedisSerializer ##### Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); 复制代码 #### Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 生成 JSON 的对比 #### # Jackson2JsonRedisSerializer 序列化的效果 127.0.0.1:6379> get 123 "[\"zz.domain.User\",{\"id\":123,\"name\":\"name\"}]" 127.0.0.1:6379> get userList "[\"java.util.LinkedList\",[[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}],[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}]]]" 复制代码 # GenericJackson2JsonRedisSerializer 序列化的效果 127.0.0.1:6379> get 123 "{\"@class\":\"zz.domain.User\",\"id\":123,\"name\":\"name\"}" 127.0.0.1:6379> get userList "[\"java.util.LinkedList\",[{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"},{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"}]]" 复制代码 ## 如何使用 ## ### 使用注解来缓存函数的结果 ### 在要缓存的方法上使用注解 `@Cacheable`、`@CachePut`、`@CacheEvict` 分别用于缓存返回数据、更新缓存数据、删除缓存数据。 package zz.service; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import zz.domain.User; /** * UserService * * @author zz * @date 2018/5/7 */ @Service @Slf4j public class UserService { public final String DEFAULT_NAME = "def"; @Cacheable(cacheNames = "user", key = "'id_'+#userId") public User get(int userId) { // get from db log.debug("[++] get userId=" + userId); User user = new User(); user.setId(userId); user.setName(DEFAULT_NAME); log.debug("[++] create default user=" + user); return user; } @CachePut(cacheNames = "user", key = "'id_'+#user.getId()") public User update(User user) { // save to db log.debug("[++] update user=" + user); return user; } @CacheEvict(cacheNames = "user", key = "'id_'+#userId") public void delete(int userId) { // delete from db log.debug("[++] delete userId=" + userId); } @CachePut(cacheNames = "user", key = "'id_'+#userId") public User updateName(int userId, String name) { // update to db log.debug("[++] updateName userId=" + userId + ", name=" + name); User user = get(userId); user.setName(name); return user; } public void innerCall(int userId) { log.debug("[++] innerCall"); get(userId); } } 复制代码 * 对函数的缓存是通过代理来实现的 : 类内部的某个函数对其他函数(即便被调用函数有 `@CacheXXX` 注解)的调用是不会走代理的,也就没有缓存。(比如 `innerCall` 调用 `get` 时不会使用缓存) 。 * 注解可以放到 Service、Dao 或 Controller 层。 * `@CacheXXX` 会缓存函数的返回值。比如 `increaseComment` 会缓存更新后的 `FeedCount`。 * 当缓存中有数据时,`@Cacheable` 注解的函数不会执行,直接返回缓存中的数据。 * `@CachePut`、`@CacheEvit` 注解的函数,无论如何都会执行。 ### 自定义缓存 ### 如果要更细粒度地控制 Redis,可以使用 `RedisTemplate`、`StringRedisTemplate` > StringRedisTemplate 是 RedisTemplate 的一个特例:key 和 value 都是 String 类型。 * RedisTemplate 默认使用 JDK 对 key 和 value 进行序列化,转成字节存入 Redis。 * StringRedisTemplate 的 key、value 本身就是 String,使用 StringRedisSerializer 将 String 转成字节存入 Redis。 当我们将 RedisTemplate 的 keySerializer 和 valueSerializer 都设置成了 StringRedisSerializer,则 RedisTemplate 和 StringRedisTemplate 的效果是相同的,就像下面的样例所示。 #### RedisTemplate 对 Redis 中各个数据结构的操作 #### * redisTemplate.opsForValue();//操作字符串 * redisTemplate.opsForHash();//操作hash * redisTemplate.opsForList();//操作list * redisTemplate.opsForSet();//操作set * redisTemplate.opsForZSet();//操作有序set package zz; import lombok.extern.slf4j.Slf4j; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.test.context.junit4.SpringRunner; import zz.domain.User; import zz.service.UserService; import java.util.LinkedList; import java.util.List; /** * zz.TestRedis * * @author zz * @date 2018/5/7 */ @SpringBootTest @RunWith(SpringRunner.class) @Slf4j public class TestRedis { @Autowired StringRedisTemplate stringRedisTemplate; @Autowired RedisTemplate redisTemplate; @Autowired UserService userService; @Test public void testSerializer() { // 1. // 这里的 opsForValue().get() 的参数必须转成 String 类型。 // 除非在 RedisConfig 中 将 keySerializer 设置成 GenericJackson2JsonRedisSerializer 等能将其他类型转换成 String 的。 // 2. // 如果切换了 RedisConfig 中的 ValueSerializer,要先用 redis-cli 将其中的旧数据删除。 // 不同 Serializer 格式之间的转换可能存在问题。 final int ID = 123; User oldUser; oldUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID)); log.debug("oldUser=" + oldUser); User user = new User(); user.setId(ID); user.setName("name"); log.debug("user=" + user); redisTemplate.opsForValue().set(String.valueOf(user.getId()), user); User newUser; newUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID)); log.debug("newUser=" + newUser); Assert.assertEquals(user.getId(), newUser.getId()); Assert.assertEquals(user.getName(), newUser.getName()); List<User> userList = new LinkedList<>(); userList.add(user); user.setId(233); user.setName("new"); userList.add(user); redisTemplate.opsForValue().set("userList", userList); List<User> newUserList; newUserList = (List<User>) redisTemplate.opsForValue().get("userList"); Assert.assertEquals(userList, newUserList); } @Test public void testSerizlizer2() { // 保存用于恢复,以免影响其他部分 RedisSerializer oldKeySerializer = redisTemplate.getKeySerializer(); RedisSerializer oldValueSerializer = redisTemplate.getValueSerializer(); RedisSerializer redisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(redisSerializer); redisTemplate.setValueSerializer(redisSerializer); final String KEY = "key"; String VALUE = "value"; redisTemplate.opsForValue().set(KEY, VALUE); Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY)); Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY)); VALUE = "Val2"; stringRedisTemplate.opsForValue().set(KEY, VALUE); Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY)); Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY)); // 恢复原本设置 redisTemplate.setKeySerializer(oldKeySerializer); redisTemplate.setValueSerializer(oldValueSerializer); } @Test public void testCache() { final int USER_ID = 1; User user = userService.get(USER_ID); log.debug("user=" + user); Assert.assertEquals(userService.DEFAULT_NAME, user.getName()); // 这次会直接返回 cache user = userService.get(USER_ID); log.debug("user=" + user); // 获得修改过的 cache final String ANOTHER_NAME = "another user"; user.setName(ANOTHER_NAME); userService.update(user); user = userService.get(USER_ID); log.debug("user=" + user); Assert.assertEquals(ANOTHER_NAME, user.getName()); // 直接调用 get 会走缓存,通过 innerCall 来调用 get 不会走缓存 log.debug("------ before"); userService.get(USER_ID); log.debug("------ middle"); userService.innerCall(USER_ID); log.debug("------ after"); // 另一种修改的方式 final String NEW_NAME = "updated"; userService.updateName(USER_ID, NEW_NAME); user = userService.get(USER_ID); log.debug("user=" + user); Assert.assertEquals(NEW_NAME, user.getName()); // 删除后,cache 中的数据会被删除,name 会变成初始值 userService.delete(USER_ID); user = userService.get(USER_ID); log.debug("user=" + user); Assert.assertEquals(userService.DEFAULT_NAME, user.getName()); // 即使 cache 中没有该数据,也会执行 delete 中的逻辑 userService.delete(USER_ID); userService.delete(USER_ID); } } 复制代码 转载于:https://juejin.im/post/5aeef9c06fb9a07a9d703c9a [spring-boot-redis]: https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Fyzbyzz%2FYetAnotherSpringBootExmaple%2Ftree%2Fmaster%2Fspring-boot-redis [STACK OVERFLOW _ String _ Hash]: https://link.juejin.im?target=https%3A%2F%2Fstackoverflow.com%2Fquestions%2F16375188%2Fredis-strings-vs-redis-hashes-to-represent-json-efficiency%23 [Spring-data-redis_ serializer]: https://link.juejin.im?target=http%3A%2F%2Fshift-alt-ctrl.iteye.com%2Fblog%2F1887370
还没有评论,来说两句吧...