SpringBoot 整合 Redis

前边我们已经学习了 Redis 的一些基本命令,以及通过 Jedis、Lettuce 来操作 Redis,但在实际的开发中,我们更多的会在 SpringBoot 中整合 Redis,来提高开发效率。

一、集成 Redis

我这里使用 SpringBoot 2.5.0版本,通过 Spring Data Redis 来集成 Redis:


    org.springframework.boot
    spring-boot-starter-data-redis

然后就是一些 Redis 的配置:

spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=shehuan

从 SpringBoot2.x 开始,默认使用 Lettuce 作为 Spring Data Redis 的内部实现,而不是 Jedis,这一点可以从spring-boot-starter-data-redis的 pom 文件看出:

如果需要使用 Jedis,则需要手动添加对应的依赖:


    redis.clients
    jedis
    3.6.0

并在配置文件中切换 Redis 客户端为 jedis:

spring.redis.client-type=jedis

最基本的配置就这些了,根据 SpringBoot 的自动装配机制,会自动的创建一些对象来方便我们操作 Redis:

  • RedisConnectionFactory,就是根据指定的配置来获取 Redis 连接的
  • RedisTemplateStringRedisTemplate,用来操作 Redis 存取数据的,既然这两个都是用来存取数据的,那肯定是有区别的,下边我们具体看一下。

二、RedisTemplate

在 Redis 中,StringRedisTemplate是专门用来存、取字符串类型数据的,它继承RedisTemplate,使用StringRedisSerializer作为序列化器。

RedisTemplate可以用来存、取自定义的复杂数据类型,当然也包括字符串类型,它默认使用JdkSerializationRedisSerializer作为 Redis 中 key、value 的序列化器,但是这个序列化器会先将 key、value 序列化成字节数组然后再存储到 Redis,导致无法通过 Redis 客户端直观的看出到底存储的是什么信息,有问题也就不好排查了,例如我们存储一个User对象:

public class User implements Serializable {
    private Interger id;
    private String name;
    private Integer age;

    public User() {
    }

    public User(Integer id, String name, Integer age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }
    // 省略get、set
}
@Service
public class MyRedisService {
    @Autowired
    RedisTemplate redisTemplate;

    public void test1() {
        redisTemplate.opsForValue().set("user", new User("zhangsan", 18));
    }
}

通过测试类执行程序:

@SpringBootTest
public class MyRedisApplicationTests {
    @Autowired
    MyRedisService myRedisService;

    @Test
    void contextLoads() {
        myRedisService.test1();
    }
}

然后在客户端查看数据,红色区域分别是存进去的 key、value:


为了解决这个问题,一般需要我们自定义RedisTemplate来覆盖框架生成的。设置 key 的序列化器为StringRedisSerializer,即将 key 序列化为字符串;至于 value 的序列化器可以使用默认的JdkSerializationRedisSerializer,也可以设置为Jackson2JsonRedisSerializer,即将 value 序列化为 json 字符串在存储。由于 Redis 中 Hash 类型数据结构的 value 也是一个 field-value 键值对,也可以分别指定序列化器。

@Configuration
public class RedisConfig {
    @Bean("redisTemplate")
    public RedisTemplate initRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        // 设置连接工厂
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 定义 String 序列化器
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // 定义 Jackson 序列化器
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        //反序列化时智能识别变量名(识别没有按驼峰格式命名的变量名)
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //反序列化识别对象类型
//        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
        //反序列化如果有多的属性,不抛出异常
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //反序列化如果碰到不识别的枚举值,是否作为空值解释,true:不会抛不识别的异常, 会赋空值,false:会抛不识别的异常
        objectMapper.configure(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL, true);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置 Redis 的 key 以及 hash 结构的 field 使用 String 序列化器
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // 设置 Redis 的 value 以及 hash 结构的 value 使用 Jackson 序列化器
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}

清空数据,再次运行测试代码后,再查看客户端数据,基本符合预期了:


注意,RedisTemplate使用JdkSerializationRedisSerializer作为 value 的默认序列化器,直接存储数字或者以字符串形式存储数字,后期都是无法使用 Redis 命令对 value 进行各种数学运算的;使用Jackson2JsonRedisSerializer作为 value 的序列化器时直接存储数字,是可以对value 进行数学运算的;StringRedisTemplate使用StringRedisSerializer作为默认的序列化器,以字符串形式存储的数字后期是可以进行数学运算的。

三、操作 Redis 数据类型

如果要存取的数据可以用字符串类型表示,建议使用StringRedisTemplate,如果是自定义的复杂对象可以使用RedisTemplate,这里的RedisTemplate是前边我们自定义的。

Spring Data Redis 中提供了如下接口,可以完成对 Redis 常见数据结构的操作:

  • ValueOperations,对应 String 数据类型,bit(bitmap/位图)操作也是用它实现
  • ListOperations,对应 List 数据类型
  • SetOperations,对应 Set 数据类型
  • HashOperations,对应 Hash 数据类型
  • ZSetOperations,对应 ZSet 数据类型
  • GeoOperations,对应 Geo 数据类型
  • HyperLogLogOperations,对应 HyperLogLog 数据类型

具体的用法也是很简单的,和 Redis 数据类型的用法基本一致,下边举几个例子:

@Service
public class MyRedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    @Autowired
    RedisTemplate redisTemplate;

    public void test2() {
        stringRedisTemplate.opsForValue().set("key1", "10");
        String key1 = stringRedisTemplate.opsForValue().get("key1");

        stringRedisTemplate.opsForSet().add("key2", "1", "2", "3");
        Boolean isMember = stringRedisTemplate.opsForSet().isMember("key2", "1");

        redisTemplate.opsForList().leftPush("user", new User("zhangsan", 18));
        redisTemplate.opsForList().leftPush("user", new User("lisi", 20));
        User user1 = (User) redisTemplate.opsForList().rightPop("user");
    }
}

使用XxxxOperations系列的接口,如果要对一个 key 的值进行多次操作,就需要多次绑定同一个 key,会麻烦一些。

针对这种情况,我们可以使用BoundKeyOperations接口的实现类来实现对一个 key 的值进行多次操作:

  • BoundValueOperations
  • BoundListOperations
  • BoundSetOperations
  • BoundHashOperations
  • BoundZSetOperations
  • BoundGeoOperations
public void test3() {
    BoundValueOperations boundValueOperations = stringRedisTemplate.boundValueOps("key1");
    boundValueOperations.set("10");
    String key1 = boundValueOperations.get();

    BoundSetOperations boundSetOperations = stringRedisTemplate.boundSetOps("key2");
    boundSetOperations.add("1", "2", "3");
    Boolean isMember = boundSetOperations.isMember("1");

    BoundListOperations boundListOperations = redisTemplate.boundListOps("user");
    boundListOperations.leftPush(new User("zhangsan", 18));
    boundListOperations.leftPush(new User("lisi", 20));
    User user1 = (User) boundListOperations.rightPop();
}

四、事务

之前的文章我们已经知道,事务中常用的命令有watchunwatchmultiexec,在 SpringBoot 也是类似的,但由于事务中往往涉及多个命令,我们要保证在同一个连接中执行所有的命令,这时需要用到SessionCallback接口,之前文章中我们用 Jedis 实现了事务,这里我们在原例子基础上修改为 SpringBoot 整合 Redis 后事务的用法:

@Service
public class MyRedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public void test4() {
        // 设置商品库存为1000件
        stringRedisTemplate.opsForValue().set("stock", "1");
        List results = stringRedisTemplate.execute(new SessionCallback>() {
            @Override
            public List execute(RedisOperations operations) throws DataAccessException {
                // 监控库存
                operations.watch("stock");
                // 获取库存
                int stock = Integer.parseInt(String.valueOf(operations.opsForValue().get("stock")));
                // 如果库存大于购买数量
                if (stock > 10) {
                    stock = stock - 10;
                } else {
                    // 取消监控
                    operations.unwatch();
                    return null;
                }
                // 开启事务
                operations.multi();
                //减扣库存
                operations.opsForValue().set("stock", String.valueOf(stock));
                // 执行事务,此处打断点,在客户端修改库存
                List results = operations.exec();
                // 如果事务执行过程中发现库存在其它地方被修改过,则返回List的大小为0
                return results;
            }
        });

        if (results == null || results.size() == 0) {
            System.out.println("库存减扣失败!");
        } else {
            System.out.println("剩余库存:" + stringRedisTemplate.opsForValue().get("stock"));
        }
    }
}

五、pipeline

前边这些例子中,Redis 命令都是逐条发送到服务器去执行的,这是 Redis 的默认策略,如果有大量的命令需要执行,这样效率显然是不高的,许多时间都会耗费在网络传输上。基于这样的情况,我们可以使用pipeline技术来优化,将指令批量发送到服务器去执行,提高效率。具体的用法如下:

@Service
public class MyRedisService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;

    public void test5() {
        stringRedisTemplate.executePipelined(new SessionCallback() {
            @Override
            public  Object execute(RedisOperations operations) throws DataAccessException {
                // 写在这里的命令会被批量发送到服务器执行
                return null;
            }
        });
    }
}

六、缓存

一般数据库操作都是效率比较低的,容易产生性能问题,可以将一些从数据库查出的数据缓存起来,重复利用,提高性能。在 Sping3.1 中引入了缓存(Cache)的功能,Sping 的缓存功能支持多种实现,Redis 是比较常用的,还有Ehcache等其它的,这里就不介绍了,用法基本一致。SpringBoot 整合 Redis 后,可以很方便的用 Redis 作为缓存的实现方式,实现数据的缓存。

除了上边 Redis 连接相关的配置外,还需要额外添加使用 Redis 作为缓存需要的配置:

# 指定缓存类型
spring.cache.type=redis
# 缓存超时时间,0为永不超时
spring.cache.redis.time-to-live=0ms

以及 Spring 缓存的依赖:


    org.springframework.boot
    spring-boot-starter-cache

在 SpringBoot 启动类上添加开启缓存的注解@EnableCaching

@SpringBootApplication
@EnableCaching
public class MyRedisApplication {
    public static void main(String[] args) {
        SpringApplication.run(MyRedisApplication.class, args);
    }
}

接下来就是如何去缓存数据了,这里涉及到如下几个注解:

  • @CacheConfig,在类上使用,表示该类中方法使用的缓存名称(可以理解为数据缓存的命名空间),除了在类上使用该注解配置缓存名称,还可以用下边三个注解在方法上配置
  • @CachePut,一般用在新增或更新业务的方法上,当数据新增或更新成功后,将方法的返回结果使用指定的 key 添加到缓存中,或更新缓存中已有的 key 的值
  • @Cacheable,一般用在查询业务的方法上,先从缓存中根据指定的 key 查询数据,如果查询到就直接返回,否则执行该方法来获取数据,最后将方法的返回结果保存到缓存
  • @CacheEvict,一般用在删除业务的方法上,默认会在方法执行结束后移除指定 key 对应的缓存数据

下边用一个例子具体看如何使用这些注解:

@Service
@CacheConfig(cacheNames = "cache1")
public class UserService {
    @Autowired
    UserDao userDao;

    @Cacheable(cacheNames = "cache2", key = "'user'+#id")
    public User getUserById(String id) {
        return userDao.getUserById(id);
    }

    @CachePut(key = "'user'+#user.id")
    public User addUser(User user) {
        return userDao.addUser(user);
    }

    @CachePut(key = "'user'+#user.id", condition = "#result != 'null'")
    public User updateUser(User user) {
        if (userDao.getUserById(user.getId()) == null) {
            return null;
        }
        return userDao.updateUser(user);
    }

    @CacheEvict(key = "'user'+#id")
    public Integer deleteUserById(String id) {
        return userDao.deleteUserById(id);
    }
}

针对这个例子做一些说明:

  • UserDao是用来模拟数据库操作的,里边的内容不重要。
  • 注解的cacheNames属性用来配置缓存的名称,方法上的配置会覆盖类上的配置。
  • @Cacheable@CachePut@CacheEvict 都配置了一个 key 属性,作为 Redis 中缓存数据的 key,key 的值是通过一个 Spring EL 表达式返回的,这样可以根据实际需求自由的指定 key 的值。
  • @CachePut还配置了一个condition属性,用作条判断,这里表示方法返回的结果不为 null 才缓存数据。当然你也可以在@Cacheable@CacheEvictcondition属性,以便在满足对应条件时才对缓存做相应操作。

最后做一个简单的测试:

@SpringBootTest
class MyRedisApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        // 查询用户
        userService.getUserById(100);
        // 添加用户
        User user = new User(102, "wangwu", 19);
        userService.addUser(user);
    }
}

在 Redis 客户端查看缓存的数据:


你可能感兴趣的:(SpringBoot 整合 Redis)