Redis一个基于内存实现的NoSql数据库,通常将其用于数据缓存以此来减少系统的压力,下面我将在SpringBoot中分别实现Redis单机模式、Redis主从复制、Redis哨兵模式、Redis-Cluster模式在SpringBoot中的集成的示例。
一、SpringBoot中集成单机版Redis
Redis的相关安装教程这里不再赘述,请参考Docker上安装Redis。示例项目源代码https://github.com/vcharfred/spring-demo的Hoxton分支的redis-template*模块中 。
添加redis的自动装配类
版本号不用写,直接使用springboot默认适配的版本即可
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 使用lettuce做redis的连接池需要额外引入这个包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>
开箱即用的配置
正如标题所说开箱即用,我们只需要在application.yml配置类里面添加如下redis配置即可
spring: redis: # 地址 host: 192.168.56.102 # 端口号 port: 3306 # 密码 password: 123456 # 超时时间,单位毫秒 timeout: 3000 # 数据库编号 database: 0 # 配置lettuce lettuce: pool: # 连接池中的最小空闲连接 min-idle: 1 # 连接池中的最大空闲连接 max-idle: 6 # 连接池最大连接数(使用负值表示没有限制,不要配置过大,否则可能会影响redis的性能) max-active: 10 # 连接池最大阻塞等待时间(使用负值表示没有限制);单位毫秒 max-wait: 1000 #关闭超时时间;单位毫秒 shutdown-timeout: 200
springboot的redis自动装配类已经配置了一个StringRedisTemplate的bean,如果操作都是使用String的数据类型,那么直接使用即可,使用示例
@Autowired private StringRedisTemplate stringRedisTemplate; /** * 增删查改示例1 */ @Override public String crud1() { ValueOperations<String, String> operations = this.stringRedisTemplate.opsForValue(); // 获取数据 String val = operations.get("student:1"); if(null==val){ log.info("缓存中数据不存在"); Student student = Student.create(); val = JSON.toJSONString(student); // 添加数据 operations.set("student:1", val); Student student2 = Student.create(); student2.setId(2L); operations.set("student:2", JSON.toJSONString(student2)); this.stringRedisTemplate.opsForList().leftPush("student_list", JSON.toJSONString(student)); this.stringRedisTemplate.opsForList().leftPush("student_list", JSON.toJSONString(student2)); }else { // 删除数据 this.stringRedisTemplate.delete("student:2"); log.info("删除缓存"); } log.info(val); // 获取列表数据 List<String> list = this.stringRedisTemplate.opsForList().range("student_list", 0, 3); log.info(JSON.toJSONString(list)); return val; }
自定义配置Redis操作的bean
StringRedisTemplate虽然可以满足需求,但是还是需要我们去序列化一下;因此我简化操作我们需要自定义配置创建RedisTemplate的bean。创建一个配置类继承CachingConfigurerSupport类。
@Configuration public class RedisConfig<K, V> extends CachingConfigurerSupport { /** * 自定义缓存注解key的生成策略。默认的生成策略是看不懂的(乱码内容) * 通过Spring 的依赖注入特性进行自定义的配置注入并且此类是一个配置类可以更多程度的自定义配置 * 这里是生成的key是:类全名.方法名 方法参数(的md5加密) */ @Bean @Override public KeyGenerator keyGenerator() { return (target, method, params) -> { StringBuilder prefix = new StringBuilder(); prefix.append(target.getClass().getName()); prefix.append(".").append(method.getName()); StringBuilder sb = new StringBuilder(); for (Object obj : params) { sb.append(obj.toString()); } return prefix.append(DigestUtils.md5DigestAsHex(sb.toString().getBytes())); }; } /** * 缓存配置管理器 */ @Bean public CacheManager cacheManager(LettuceConnectionFactory factory) { //以锁写入的方式创建RedisCacheWriter对象 RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(factory); //设置缓存注解的缓存时间,缓存1小时 Duration duration = Duration.ofSeconds(3600L); RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(duration); return new RedisCacheManager(writer, redisCacheConfiguration); } /** * 修改redisTemplate的序列化方式 * * @param factory LettuceConnectionFactory */ @Bean(name = "redisTemplate") public RedisTemplate<K, V> redisTemplate(LettuceConnectionFactory factory) { //创建RedisTemplate对象 RedisTemplate<K, V> template = new RedisTemplate<K, V>(); template.setConnectionFactory(factory); //设置key的序列化方式 template.setKeySerializer(keySerializer()); template.setHashKeySerializer(keySerializer()); //设置RedisTemplate的Value序列化方式Jackson2JsonRedisSerializer;默认是JdkSerializationRedisSerializer template.setValueSerializer(valueSerializer()); template.setHashValueSerializer(valueSerializer()); template.afterPropertiesSet(); return template; } private RedisSerializer<String> keySerializer() { return new StringRedisSerializer(); } private RedisSerializer<Object> valueSerializer() { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class); ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会抛出异常 om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); //解决时间序列化问题 om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); om.registerModule(new JavaTimeModule()); jackson2JsonRedisSerializer.setObjectMapper(om); return jackson2JsonRedisSerializer; } }
使用示例
@Autowired private RedisTemplate<String, Student> redisTemplate; @Override public Student crud2() { ValueOperations<String, Student> operations = this.redisTemplate.opsForValue(); // 获取数据 Student val = operations.get("student2:1"); if(null==val){ log.info("缓存中数据不存在"); val = Student.create(); // 添加数据 operations.set("student2:1", val); Student student2 = Student.create(); student2.setId(2L); operations.set("student2:2", student2); this.redisTemplate.opsForList().leftPush("student_list2", val); this.redisTemplate.opsForList().leftPush("student_list2", student2); }else { // 删除数据 this.redisTemplate.delete("student2:2"); log.info("删除缓存"); } log.info(JSON.toJSONString(val)); // 获取列表数据 List<Student> list = this.redisTemplate.opsForList().range("student_list2", 0, 3); log.info(JSON.toJSONString(list)); return val; }
二、SpringBoot中实现Redis的读写分离
spring-boot-starter-data-redis 原生并不支持读写分离;因此需要我们去手动封装实现;实现思路:1.通过AOP来实现;2.封装工具类,我们在工具类里面实现;3.由使用者自己去处理。这里我们通过再次封装redisTemplate类来实现,简化调用者的工作。由于需要读写分离,因此我们需要自定义配置来实现。代码如下:
redis自定义属性配置RedisReadWriteConfig
@Data @Component @ConfigurationProperties(prefix = "spring.redis") public class RedisReadWriteConfig implements Serializable { /** * 主机地址 */ @Value("${spring.redis.host}") private String host; /** * 认证密码 */ @Value("${spring.redis.password:#{null}}") private String password; /** * 端口号 */ @Value("${spring.redis.port:6379}") private int port = 6379; /** * 数据库编号 */ @Value("${spring.redis.database:0}") private int database; /** * 连接超时时间,单位毫秒 */ @Value("${spring.redis.timeout:3000}") private long timeout; /** * 关闭超时时间,单位毫秒 */ @Value("${spring.redis.lettuce.shutdown-timeout:200}") private long shutdownTimeout; /** * 连接池中的最小空闲连接 */ @Value("${spring.redis.lettuce.pool.min-idle:1}") private int minIdle; /** * 连接池中的最大空闲连接 */ @Value("${spring.redis.lettuce.pool.max-idle:6}") private int maxIdle = 6; /** * 连接池最大连接数(使用负值表示没有限制,不要配置过大,否则可能会影响redis的性能) */ @Value("${spring.redis.lettuce.pool.max-active:10}") private int maxActive = 10; /** * 连接池最大阻塞等待时间(使用负值表示没有限制),单位毫秒 */ @Value("${spring.redis.lettuce.pool.max-wait:1000}") private long maxWait = 1000; /** * redis只读库配置 */ private List<RedisReadConfig> redisReadConfigs; @Data @Validated @AllArgsConstructor @NoArgsConstructor static class RedisReadConfig { /** * 主机地址 */ @NotEmpty private String host; /** * 认证密码 */ private String password; /** * 端口号 */ private int port = 6379; /** * 数据库编号 */ private int database = 0; } /** * 只读实例配置 * * @return 返回所有数据读取的配置 */ public List<RedisStandaloneConfiguration> buildReadRedisStandaloneConfiguration() { if (enableReadWriteModel()) { redisReadConfigs = redisReadConfigs.stream().distinct().collect(Collectors.toList()); List<RedisStandaloneConfiguration> list = new ArrayList<>(redisReadConfigs.size()); for(RedisReadConfig readConfig : redisReadConfigs){ list.add(createRedisStandaloneConfiguration(readConfig)); } return list; } return null; } /** * 写实例配置 * * @return 返回所有数据读取的配置 */ public RedisStandaloneConfiguration buildWriteRedisStandaloneConfiguration() { RedisReadConfig redisServerConfig = new RedisReadConfig(); redisServerConfig.setHost(this.host); redisServerConfig.setPort(this.port); redisServerConfig.setPassword(this.password); redisServerConfig.setDatabase(this.database); return createRedisStandaloneConfiguration(redisServerConfig); } /** * 是否启动读写分离模式 * * @return 启用返回true;否则false */ public boolean enableReadWriteModel(){ return redisReadConfigs != null && !redisReadConfigs.isEmpty(); } private RedisStandaloneConfiguration createRedisStandaloneConfiguration(RedisReadConfig redisServerConfig) { RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); // 连接地址 redisStandaloneConfiguration.setHostName(redisServerConfig.getHost()); // 认证密码 redisStandaloneConfiguration.setPassword(redisServerConfig.getPassword()); // 端口号,默认6379 redisStandaloneConfiguration.setPort(redisServerConfig.getPort()); // 数据库编号 redisStandaloneConfiguration.setDatabase(redisServerConfig.getDatabase()); return redisStandaloneConfiguration; } }
配置示例
server: port: 8088 spring: redis: # 地址 host: 192.168.56.102 # 端口号 port: 6379 # 密码 password: 123456 # 超时时间,单位毫秒 timeout: 3000 # 数据库编号 database: 0 # 配置lettuce lettuce: pool: # 连接池中的最小空闲连接 min-idle: 1 # 连接池中的最大空闲连接 max-idle: 6 # 连接池最大连接数(使用负值表示没有限制,不要配置过大,否则可能会影响redis的性能) max-active: 10 # 连接池最大阻塞等待时间(使用负值表示没有限制);单位毫秒 max-wait: 1000 #关闭超时时间;单位毫秒 shutdown-timeout: 200 # redis只读库配置 redis-read-configs: - host: 192.168.56.104 port: 6379 password: 123456 - host: 192.168.56.105 port: 6379 password: 123456
自定义redis连接工厂ReadWriteLettuceConnectionFactory
由于我们有多个只读库,为了实现动态切换,我们自己是实现一个工厂,方便后面操作
@Slf4j @Component public class ReadWriteLettuceConnectionFactory implements DisposableBean { private final LettuceConnectionFactory writeConnectionFactory; private final List<LettuceConnectionFactory> readConnectionFactories = new ArrayList<>(); private final AtomicInteger pos = new AtomicInteger(); private int max = -1; public ReadWriteLettuceConnectionFactory(RedisReadWriteConfig readWriteConfig) { this.writeConnectionFactory = createLettuceConnectionFactory(readWriteConfig, readWriteConfig.buildWriteRedisStandaloneConfiguration()); Assert.notNull(writeConnectionFactory, "redis config can not null, if don't used please remove dependence redis jar."); if (readWriteConfig.enableReadWriteModel()) { List<RedisStandaloneConfiguration> list = readWriteConfig.buildReadRedisStandaloneConfiguration(); if(null!=list){ for(RedisStandaloneConfiguration rsc:list){ LettuceConnectionFactory connectionFactory = createLettuceConnectionFactory(readWriteConfig, rsc); if(connectionFactory!=null){ log.info("redis-read-datasource - load a datasource [{}:{}] success!", rsc.getHostName(), rsc.getPort()); readConnectionFactories.add(connectionFactory); max++; }else { log.warn("redis-read-datasource - load a datasource [{}:{}] fail!", rsc.getHostName(), rsc.getPort()); } } }else { log.warn("found read redis config, but can't load a datasource fail!"); } } } /** * 获取读连接池 * @return 返回连接工厂 */ public LettuceConnectionFactory getReadFactory() { // 简单的负载均衡:轮询方案 if(pos.get()>max){ pos.set(0); } int index = pos.getAndIncrement(); log.info("chose redis-read-datasource index is [{}]", pos); return getReadFactory(index); } private LettuceConnectionFactory getReadFactory(int index) { if(index>max){ log.warn("no suitable redis-read-datasource [{}], the max {}.", index, max); // 发生错误自动切换到写连接上去 return writeConnectionFactory; } return readConnectionFactories.get(index); } /** * 获取写连接池 * @return 返回连接工厂 */ public LettuceConnectionFactory getWriteFactory() { return writeConnectionFactory; } /** * 创建Lettuce连接工厂 * * @param readWriteConfig redis配置 * @param redisStandaloneConfiguration redis独立配置 * @return 返回连接工厂 */ private LettuceConnectionFactory createLettuceConnectionFactory(RedisReadWriteConfig readWriteConfig , RedisStandaloneConfiguration redisStandaloneConfiguration) { if (redisStandaloneConfiguration == null) { return null; } // 连接池配置 GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); // 连接池中的最小空闲连接 poolConfig.setMinIdle(readWriteConfig.getMinIdle()); // 连接池中的最大空闲连接 poolConfig.setMaxIdle(readWriteConfig.getMaxIdle()); // 连接池最大连接数(使用负值表示没有限制,不要配置过大,否则可能会影响redis的性能) poolConfig.setMaxTotal(readWriteConfig.getMaxActive()); // 连接池最大阻塞等待时间(使用负值表示没有限制) poolConfig.setMaxWaitMillis(readWriteConfig.getMaxWait()); LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder lettucePoolingClientConfigurationBuilder = LettucePoolingClientConfiguration.builder(); // 连接池配置 lettucePoolingClientConfigurationBuilder.poolConfig(poolConfig); // 关闭超时时间,单位毫秒 lettucePoolingClientConfigurationBuilder.shutdownTimeout(Duration.ofMillis(readWriteConfig.getShutdownTimeout())); // 超时时间,单位毫秒 lettucePoolingClientConfigurationBuilder.commandTimeout(Duration.ofMillis(readWriteConfig.getTimeout())); LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration , lettucePoolingClientConfigurationBuilder.build()); lettuceConnectionFactory.afterPropertiesSet(); return lettuceConnectionFactory; } @Override public void destroy() throws Exception { writeConnectionFactory.destroy(); if(!readConnectionFactories.isEmpty()){ for(LettuceConnectionFactory connectionFactory:readConnectionFactories){ connectionFactory.destroy(); } readConnectionFactories.clear(); } log.info("redis-datasource all closed success."); } }
重写RedisTemplate类实现多个只读库的动态切换
为了实现多个只读库能够自动的切换和轮询,需要对原来的RedisTemplate类做些升级,通过查看其源码发现每次执行相关操作前都会调用getRequiredConnectionFactory()这个方法;基于此我们通过继承RedisTemplate然后重写其getRequiredConnectionFactory()方法来实现只读库的切换。
public class ReadWriteRedisTemplate<K, V> extends RedisTemplate<K, V> { private ReadWriteLettuceConnectionFactory readWriteConnectionFactory; private boolean isRead = false; /** * RedisTemplate每次执行方法时都会调用这个方法;如果只有1读1写,那么就没有必要再弄这个封装类,直接在创建的时候指定即可 * * @return RedisConnectionFactory */ @Override public RedisConnectionFactory getRequiredConnectionFactory() { return getFactory(); } public void setReadWriteConnectionFactory(ReadWriteLettuceConnectionFactory readWriteConnectionFactory, boolean isRead) { this.isRead = isRead; this.readWriteConnectionFactory = readWriteConnectionFactory; setConnectionFactory(getFactory()); } private RedisConnectionFactory getFactory(){ if(this.isRead){ return this.readWriteConnectionFactory.getReadFactory(); } return this.readWriteConnectionFactory.getWriteFactory(); } }
配置RedisTemplate的注入bean
@Configuration public class RedisCachingConfigurer<K, V> extends CachingConfigurerSupport { /** * 读数据的RedisTemplate * * @param factory LettuceConnectionFactory */ @Bean(name = "readRedisTemplate") public RedisTemplate<K, V> readRedisTemplate(ReadWriteLettuceConnectionFactory factory) { return redisTemplate(factory, true); } /** * 写数据的RedisTemplate * * @param factory LettuceConnectionFactory */ @Bean(name = "writeRedisTemplate") public RedisTemplate<K, V> writeRedisTemplate(ReadWriteLettuceConnectionFactory factory) { return redisTemplate(factory, false); } private RedisTemplate<K, V> redisTemplate(ReadWriteLettuceConnectionFactory factory, boolean isRead) { //创建Redis缓存操作助手RedisTemplate对象 ReadWriteRedisTemplate<K, V> template = new ReadWriteRedisTemplate<K, V>(); template.setReadWriteConnectionFactory(factory, isRead); //设置key的序列化方式 template.setKeySerializer(keySerializer()); template.setHashKeySerializer(keySerializer()); //将RedisTemplate的Value序列化方式由JdkSerializationRedisSerializer更换为Jackson2JsonRedisSerializer template.setValueSerializer(valueSerializer()); template.setHashValueSerializer(valueSerializer()); template.afterPro
发表评论 取消回复