Spring Boot集成Redis内存数据库

常规的业务数据,一般选择存储在SQL数据库中。

传统的SQL数据库基于磁盘存储,可以正常的流量需求。然而,在高并发应用场景中容易被拖垮,导致系统崩溃。

针对这种情况,我们可以通过增加缓存、使用NoSQL数据库等方式进行优化。

Redis是一款开源的内存NoSQL数据库,其稳定性高、性能强悍),是KV细分领域的市场占有率冠军

本节将介绍Redis与Spring Boot的集成方式。

Redis环境准备

与前文类似,我们使用Docker快速部署Redis服务器。

  1. #!/bin/bash
  2. NAME="redis"
  3. PUID="1000"
  4. PGID="1000"
  5. VOLUME="$HOME/docker_data/redis"
  6. mkdir -p $VOLUME
  7. docker ps -q -a --filter "name=$NAME" | xargs -I {} docker rm -f {}
  8. docker run \
  9. --hostname $NAME \
  10. --name $NAME \
  11. --volume "$VOLUME":/data \
  12. -p 6379:6379 \
  13. --detach \
  14. --restart always \
  15. redis:6 \
  16. redis-server --appendonly yes --requirepass redisdemo

在上述脚本中:

  • 使用了最新的redis 6镜像

  • 开启”appendonly”的持久化方式

  • 启用密码”redisdemo”

  • 端口暴露为6379

我们尝试连接一下:

  1. redis-cli -h 127.0.0.1 -a redisdemo

成功!(如果你没有redis-cli的可执行文件,可以到官网下载)

Redis的缓存使用

Spring提供了内置的Cache框架,可以通过@Cache注解,轻松实现redis Cache的功能。

首先引入依赖:

  1. implementation 'org.springframework.boot:spring-boot-starter-data-redis'
  2. implementation 'org.springframework.boot:spring-boot-starter-json'
  3. implementation 'org.apache.commons:commons-pool2:2.11.0'

上述依赖的作用分别为:

  • redis客户端:Spring Boot 2使用的是lettuce

  • json依赖:我们要使用jackson做json的序列化 / 反序列化

  • commons-pool2线程池,这里其实是data-redis没处理好,需要额外加入,按理说应该集成在starter里的

接着我们在application.yaml中定义数据源:

  1. # redis demo
  2. spring:
  3. redis:
  4. host: 127.0.0.1
  5. port: 6379
  6. password: "redisdemo"
  7. lettuce:
  8. pool:
  9. max-active: 50
  10. min-idle: 5

接着我们需要设置自定义的Configuration:

  1. package com.coder4.homs.demo.server.configuration;
  2. import com.fasterxml.jackson.annotation.JsonAutoDetect;
  3. import com.fasterxml.jackson.annotation.PropertyAccessor;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping;
  6. import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
  7. import org.springframework.cache.annotation.CachingConfigurerSupport;
  8. import org.springframework.cache.annotation.EnableCaching;
  9. import org.springframework.cache.interceptor.KeyGenerator;
  10. import org.springframework.context.annotation.Bean;
  11. import org.springframework.context.annotation.Configuration;
  12. import org.springframework.data.redis.cache.RedisCacheConfiguration;
  13. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
  14. import org.springframework.data.redis.serializer.RedisSerializationContext;
  15. import java.time.Duration;
  16. /**
  17. * @author coder4
  18. */
  19. @Configuration
  20. @EnableCaching
  21. public class RedisCacheCustomConfiguration extends CachingConfigurerSupport {
  22. @Bean
  23. public KeyGenerator keyGenerator() {
  24. return (target, method, params) -> {
  25. StringBuilder sb = new StringBuilder();
  26. // sb.append(target.getClass().getName());
  27. sb.append(target.getClass().getSimpleName());
  28. sb.append(":");
  29. sb.append(method.getName());
  30. for (Object obj : params) {
  31. sb.append(obj.toString());
  32. sb.append(":");
  33. }
  34. sb.deleteCharAt(sb.length() - 1);
  35. return sb.toString();
  36. };
  37. }
  38. @Bean
  39. public RedisCacheConfiguration redisCacheConfiguration() {
  40. Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
  41. ObjectMapper objectMapper = new ObjectMapper();
  42. objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
  43. objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, DefaultTyping.NON_FINAL);
  44. // use json serde
  45. serializer.setObjectMapper(objectMapper);
  46. return RedisCacheConfiguration.defaultCacheConfig()
  47. .entryTtl(Duration.ofMinutes(5)) // 5 mins ttl
  48. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));
  49. }
  50. }

上述主要包含两部分:

  • KeyGenerator可以根据Class + method + 参数 生成唯一的key名字,用于Redis中存储的key

  • RedisCacheConfiguration做了2处定制:

    • 更改了序列化方式,从默认的Java(Serilization更改为Jackson(json)

    • 缓存过期时间为5分钟

接着,我们在项目中使用Cache

  1. public interface UserRepository {
  2. Optional<Long> create(User user);
  3. @Cacheable(value = "cache")
  4. Optional<User> getUser(long id);
  5. Optional<User> getUserByName(String name);
  6. }

这里我们用了@Cache注解,”cache”是key的前缀

访问一下:

  1. curl http://127.0.0.1:8080/users/1

然后看一下redis

  1. redis-cli -a redisdemo
  2. > keys *
  3. > "cache::UserRepository1Impl:getUser1"
  4. > get "cache::UserRepository1Impl:getUser1"
  5. "[\"com.coder4.homs.demo.server.model.User\",{\"id\":1,\"name\":\"user1\"}]"
  6. > ttl "cache::UserRepository1Impl:getUser1"
  7. > 293

数据被成功缓存在了Redis中(序列化为json),并且会自动过期。

我们使用Spring Boot集成SQL数据库2一节中的压测脚本验证性能,QPS达到860,提升达80%。

在数据发生删除、更新时,你需要更新缓存,以确保一致性。推荐你阅读缓存更新的套路)。

在更新/删除方法上应用@CacheEvict(beforeInvocation=false),可以实现更新时删除的功能。

Redis的持久化使用

Redis不仅可以用作缓存,也可以用作持久化的存储。

首先请确认Redis已开启持久化:

  1. 127.0.0.1:6379> config get save
  2. 1) "save"
  3. 2) "3600 1 300 100 60 10000"
  4. 127.0.0.1:6379> config get appendonly
  5. 1) "appendonly"
  6. 2) "yes"

上述分别为rdb和aof的配置,有任意一个非空,即表示开启了持久化。

实际上,在我们集成Spring Data的时候,会自动配置RedisTemplte,使用它即可完成Redis的持久化读取。

不过默认配置的Template有一些缺点,我们需要做一些改造:

  1. package com.coder4.homs.demo.server.configuration;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.data.redis.core.RedisTemplate;
  5. import org.springframework.data.redis.serializer.RedisSerializer;
  6. import org.springframework.data.redis.serializer.StringRedisSerializer;
  7. /**
  8. * @author coder4
  9. */
  10. @Configuration
  11. public class RedisTemplateConfiguration {
  12. @Autowired
  13. public void decorateRedisTemplate(RedisTemplate redisTemplate) {
  14. RedisSerializer stringSerializer = new StringRedisSerializer();
  15. redisTemplate.setKeySerializer(stringSerializer);
  16. redisTemplate.setKeySerializer(stringSerializer);
  17. redisTemplate.setValueSerializer(stringSerializer);
  18. redisTemplate.setHashKeySerializer(stringSerializer);
  19. redisTemplate.setHashValueSerializer(stringSerializer);
  20. }
  21. }

如上所述,我们设置RedisTemplate的KV,分别采用String的序列化方式。

接着我们在代码中使用其存取Redis:

  1. @Autowired
  2. private RedisTemplate redisTemplate;
  3. redisTemplate.boundValueOps("key").set("value");

RedisTemplate的语法稍微有些奇怪,你也可以直接使用Conn来做操作,这样更加”Lettuce”。

  1. @Autowired
  2. private LettuceConnectionFactory leconnFactory;
  3. try (RedisConnection conn = leconnFactory.getConnection()) {
  4. conn.set("hehe".getBytes(), "haha".getBytes());
  5. }

至此,我们已经完成了Spring Boot 与 Redis的集成。

思考题:当一个微服务需要连接多组Redis,该如何集成呢?

请自己探索,并验证其正确性。