官方介绍
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;
@Autowired
public 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 {
@Autowired
UserDao 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 {
@Autowired
IUserService userService;
@Test
void findOneById() {
User one = userService.findOneById(2L);
System.out.println(one);
}
@Test
void deleteUserById() {
userService.deleteUserById(1l);
}
@Test
void updateUser() {
User lm2 = new User(1l, "lm2", "123", 23l);
userService.updateUser(lm2);
}
}
读者自测,反正作者测试成功了。
源码地址:https://gitee.com/liu_liangyuan/spring-boot_learning/tree/master/springboot_redis
补充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")
@Bean
public 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;
}
}