官方介绍

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>

有些依赖是做测试的。

搭建如下目录结构:

redisson读写 多个值 redis 读取_User


编写实体类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;
    }

}