spring boot 2.x整合redis(原理),以及开启json数据缓存和读取

官方介绍

12.1. Redis
Redis is a cache, message broker, and richly-featured key-value store. Spring Boot offers basic auto-configuration for the Lettuce and Jedis client libraries and the abstractions on top of them provided by Spring Data Redis.
作者翻译:rides是一个缓存、消息中间件和功能丰富的k-v存储器,Spring Boot通过spring data reids为jedis 和 lettuce客户端库提供了基本的auto configuration自动配置。(jedis和lettuce是redis的客户端,也就是用来链接redis 服务器的。)
There is a spring-boot-starter-data-redis “Starter” for collecting the dependencies in a convenient way. By default, it uses Lettuce. That starter handles both traditional and reactive applications.

作者翻译:有一个spring-boot-starter-redis启动器方便的集成了这些依赖,默认用lettuce客户端,这个启动器既可以处理传统的应用,也可以处理响应式应用。

We also provide a spring-boot-starter-data-redis-reactive “Starter” for consistency with the other stores with reactive support.

作者翻译:我们也提供了一个spring-boot-starter-data-redis-reactive启动器用于和其他的响应式存储保持数据(作者想了想,这里应该加一个数据单词,虽然原句中没有提到数据)一致。
12.1.1. Connecting to Redis(链接redis)
You can inject an auto-configured RedisConnectionFactory, StringRedisTemplate, or vanilla RedisTemplate instance as you would any other Spring Bean. By default, the instance tries to connect to a Redis server at localhost:6379. The following listing shows an example of such a bean:

作者翻译:你能把auto-configured RedisConnectionFactory、StringRedisTemplate(自动配置的RedisConnectionFactory、StringRedisTemplate或者vannilla RedisTemplate )实例注入到任何你想操作的bean中,默认你注入的这个实例尝试去链接一个redis-server(redis服务器)在localhost:6279上。下面代码展示了这种bean的使用:

@Component
public class MyBean {private StringRedisTemplate template;@Autowiredpublic MyBean(StringRedisTemplate template) {this.template = template;}// ...}

You can also register an arbitrary number of beans that implement LettuceClientConfigurationBuilderCustomizer for more advanced customizations. If you use Jedis, JedisClientConfigurationBuilderCustomizer is also available.
If you add your own @Bean of any of the auto-configured types, it replaces the default (except in the case of RedisTemplate, when the exclusion is based on the bean name, redisTemplate, not its type). By default, if commons-pool2 is on the classpath, you get a pooled connection factory.

作者翻译:你也能注册一个任意数量的bean来实现LettuceClientConfigurationBuilderCustomizer接口,用以自定义更高级的功能。如果你用jedis,实现JedisClientConfigurationBuilderCustomizer用来定义自己的想要的功能。如果你添加了用@bean注解的自动配置类,自动配置类会替代默认的配置(除非在RedisTemplate的情况下,当排除是基于bean的名称,RedisTemplate,而不是它的类型)。默认,如果你配置了commons-pool2在你的类路径下,你会获得一个连接池。

作者再把上面那段英文翻译一遍,因为半直译翻出来的,根本读的不爽,如下再意译一遍:
你能注册任意数量实现LettuceClientConfigurationBuilderCustomizer接口的bean用来做自己的配置文件,实现自己想要的功能。如果你用的是jedis客户端,你只需要实现JedisClientConfigurationBuilderCustomizer接口也能完成自定义配置。如果你添加了自动配置类,它会覆盖原来默认的配置(在用redistemplate的情况下,会排除你配置的自动配置类,注意是用redistemplate类,不是它的类型)。默认如果你在类路径下引用了commons-pool2框架,你就会获得一个链接池用来连接redis-server(redis服务器)。

原理刨析

官方文档说的很清楚,spring boot为redis提供了一个spring boot data starter redis启动器,用来简化redis整合spring boot的麻烦。
默认提供了两个客户端,分别是jedis和lettuce,其中默认使用的是lettuce。

jedis和lettuce的区别是什么呢?一句话,jedis是线程不安全的,而lettuce是线程安全的,为什么呢?
jedis是采用直接链接redis-server的方式来操作redis的,再Java程序中,多个线程公用一个jedis客户端,当然会造成线程不安全了。
lettuce和jedis的底部构造不一样,lettuce是基于netty开发的,底层用的是Java的NIO,而且netty开发的本意就是用来提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序,所以说lettuce是线程安全的。

在spring boot中为什么会先加载lettuce,而不是jedis呢?
调出lettuceConnectionConfigur.java部分源码如下所示:

@Configuration(proxyBeanMethods = false
)
@ConditionalOnClass({RedisClient.class})
@ConditionalOnProperty(name = {"spring.redis.client-type"},havingValue = "lettuce",matchIfMissing = true
)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {LettuceConnectionConfiguration(RedisProperties properties, ObjectProvider<RedisSentinelConfiguration> sentinelConfigurationProvider, ObjectProvider<RedisClusterConfiguration> clusterConfigurationProvider) {super(properties, sentinelConfigurationProvider, clusterConfigurationProvider);}

@ConditionOnClass注解的作用表示在什么情况下,下面的类生效。上面源码表示如果redisclient.class在类路径被加载,则底下的类生效。在引入spring boot starter data redis启动器时,redisclient就被加载了。
再打开JedisConnectionConfigur.java源码如下所示:

@Configuration(proxyBeanMethods = false
)
@ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
@ConditionalOnMissingBean({RedisConnectionFactory.class})
@ConditionalOnProperty(name = {"spring.redis.client-type"},havingValue = "jedis",matchIfMissing = true
)
class JedisConnectionConfiguration extends RedisConnectionConfiguration {JedisConnectionConfiguration(RedisProperties properties, ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration, ObjectProvider<RedisClusterConfiguration> clusterConfiguration) {super(properties, sentinelConfiguration, clusterConfiguration);}

@ConditionalOnMissingBean表示类路径下没有RedisConnectionFactory类时,低下的配置类生效。上面@ConditionalOnClass注解中,表示GenericObjectPool,JedisConnection,Jedis存在类路径下时,低下的配置类才生效。其中genericObjectPool.java在common-pool2中,而common-pool2框架只有你自己引入到pom.xml文件中时才生效。

所以由上总结出:引入common-pool2框架后,Jedis生效,如果common-pool2不引入则lettuce生效。

上代码

创建数据库和表的SQL代码

create database lm;
use lm;
create table User(id long auto_increase primary key,username varchar(255) not null,password varchar(16) not null,age long not null
)charset = utf-8;
-- 手搓代码真特么累,读者根据我给出的实体类直接编写吧!!!

搭建一个spring boot项目,引入如下依赖到pom.xml文件中:

<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.1.4</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId></dependency></dependencies>

有些依赖是做测试的。
搭建如下目录结构:

编写实体类User.java

package com.lm.domain;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;/*** @author  dell* @date 2020-12-18*/@Data
@NoArgsConstructor
@AllArgsConstructor
public class User  implements Serializable {private static final long serialVersionUID =  6418265650327364343L;public User(String username, String password, Long age) {this.username = username;this.password = password;this.age = age;}private Long id;private String username;private String password;private Long age;}

编写UserDao.java接口

package com.lm.dao;import com.lm.domain.User;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import org.springframework.stereotype.Repository;import java.util.List;@Repository
public interface UserDao {@Select("select * from User where id = #{id}")List<User> findById(Long id);@Insert("insert into User(username,password,age) values(#{username},#{password},#{age})")Long addUser(User user);@Update("update set u.username = #{username},u.password = #{password},u.age = #{age} from User u where u.id = #{id}")Long updateUser(User user);@Delete("delete from User u where u.id = #{id}")Long deleteUser(User user);
}

编写IUserService.java接口和UserService.java操作类

package com.lm.service;import com.lm.domain.User;public interface IUserService {User findOneById(Long id);void deleteUserById(Long id);User updateUser(User user);
}
package com.lm.service.impl;import com.lm.dao.UserDao;
import com.lm.domain.User;
import com.lm.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;import java.util.List;@Transactional(propagation = Propagation.REQUIRED)
@Service
public class UserService implements IUserService {@AutowiredUserDao userDao;@Cacheable(cacheNames = "user", key = "#p0")public User findOneById(Long id) {System.out.println("service running this method");List<User> users = userDao.findById(id);System.out.println(users);if (users.isEmpty()) return null;return users.get(0);}@CacheEvict(cacheNames = "user", key = "#p0")public void deleteUserById(Long id) {userDao.deleteUser(new User(id, "", "", 0l));}@CachePut(cacheNames = "user", key = "#root.args.id")public User updateUser(User user) {Long l = userDao.updateUser(user);if (user.getId() == l) return user;user.setId(l);return user;}
}

再给出测试类如下:

package com.lm.service.impl;import com.lm.domain.User;
import com.lm.service.IUserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class UserServiceTest {@AutowiredIUserService userService;@Testvoid findOneById() {User one = userService.findOneById(2L);System.out.println(one);}@Testvoid deleteUserById() {userService.deleteUserById(1l);}@Testvoid updateUser() {User lm2 = new User(1l, "lm2", "123", 23l);userService.updateUser(lm2);}
}

读者自测,反正作者测试成功了。


源码地址:


补充redis json支持配置类,此配置类会替代默认的配置类来进行缓存和数据之间的转换

package com.lm.config;import java.time.Duration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;@Configuration
public class MyRedisConfig extends CachingConfigurerSupport  {/*** 配置redisCacheManager* 在redis中,提供操作redis的两个bean分别为redistemplate和stringRedisTemplate,两个类没啥区别* 在spring boot starter redis中,提供了两个链接redis-server的客户端,分别为jedis和lettuce,前者是线程不安全的,后者是线程安全的;* jedis是直接链接redis-server的,当多个线程操作一个jedis实例时,容易造成线程不安全。* lettuce是基于netty设计的,netty又是基于nio设计的,所以lettuce是线程安全的。* @param factory* @return*/@SuppressWarnings("deprecation")@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);//解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置序列化(解决乱码的问题),过期时间30秒RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(30)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).disableCachingNullValues();RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();return cacheManager;}}