第3章〓Spring Redis Template
视频讲解
视频讲解
RedisTemplate类可以用于简化Redis操作,是Spring Data Redis对Redis支持的核心类。它可以负责序列化和连接管理,用户无须关心此类细节。此外,该类提供了Redis操作视图(依据Redis命令分组),这些视图提供了丰富的通用接口,可以以程序的方式执行Redis命令。RedisTemplate一旦配置好就是线程安全的。
本章将围绕RedisTemplate类的连接建立、Redis操作视图、键绑定、序列化等内容展开。
3.1Java Redis客户端
为了能够在应用程序中使用Redis,通过Redis命令操作是不现实的。Redis提供了适配不同编程语言的客户端实现通过编程操作Redis。Java有很多优秀的Redis客户端,如Lettuce、Jedis、Redisson和JRedis等。表31列举了常用的Java Redis客户端的特性。
表31常用的Java Redis客户端的特性
客户端框架整合介绍
LettuceSpring Data RedisLettuce是基于Netty实现的,支持同步、异步和响应编程方式并且是线程安全的,支持Redis的哨兵模式、集群模式和流水线
JedisSpring Data Redis以Redis命令作为方法名称,学习成本低,简单实用。Jedis实例不是线程安全的,多线程环境下需要基于连接池来使用
Redisson/Redisson是分布式Redis客户端,底层使用Netty框架,支持Redis的哨兵模式、主从模式和单节点模式
使用Java操作Redis最常用的是使用Jedis客户端。如果在项目中使用了Jedis,但是后来决定弃用Jedis改用其他的Redis客户端就比较麻烦了。因为不同的Java Redis客户端是无法兼容的。Spring Data Redis是Spring Data模块的一部分,专门用来支持在Spring管理的项目中对Redis的操作。Spring Data Redis提供了Redis的Java客户端的抽象,在开发中可以忽略由于切换Redis客户端所带来的影响,而且它本身就属于Spring的一部分,比起单纯的使用Jedis更加稳定,管理起来更加自动化。
使用Spring Data Redis的首要任务之一是通过Spring容器连接到Redis。为此,需要创建一个Java连接器。无论开发者选择哪个Java Redis客户端,只需要使用一组Spring Data Redis API(在所有连接器中表现一致)。即使用org.springframework.data.redis.connection包中RedisConnection和RedisConnectionFactory接口。这两个接口用于处理和获取Redis的活动连接。
RedisConnection接口为应用程序与Redis通信提供了核心组件。因为它处理与Redis后端的通信。它还自动将底层连接库异常转换为与Spring一致的DAO异常层次结构,这样就可以在不更改任何代码的情况下切换连接器,因为操作语义保持不变。
RedisConnection对象是通过RedisConnectionFactory(工厂)创建的。此外,工厂充当PersistenceExceptionTranslator对象。这意味着一旦声明,PersistenceExceptionTranslator对象就允许开发者进行透明的异常转换——例如,通过使用@Repository注解和AOP进行异常转换。使用RedisConnectionFactory的最简单的方式就是通过Spring容器配置一个合适的连接器并将连接器注入给需要使用它的类。
Spring Data Redis主要有以下特性:
(1) 提供了一个可以跨越多个客户端(如Jedis、Lettuce)的底层抽象连接包。
(2) 针对数据的序列化和反序列化,提供了多种方案供开发者选择。
(3) 提供了一个RedisTemplate类,该类对Redis的各种操作、异常转换和序列化都实现了高层封装。
(4) 支持Redis的哨兵模式和集群模式。
3.2创建Redis连接
针对不同的Redis客户端,使用程序连接Redis也有不同的方式。本节讲授如何利用Lettuce、Jedis客户端以及Redis Template类创建Redis连接。
3.2.1Lettuce
Lettuce是一个可扩展的线程安全Redis客户端,用于同步、异步和响应式使用。如果多个线程不使用阻塞和事务操作(如BLPOP和MULTI/EXEC),则它们可能共享一个连接。Lettuce是基于Netty构建的。Lettuce支持Redis的高级功能,如哨兵(Sentinel)、集群(Cluster)、流水线(Pipelining)、自动重新连接和Redis数据模型。
要利用Spring Data Redis配置Lettuce连接器,首先在Maven项目中引入相关依赖,内容如下:
1
2
3org.springframework
4spring-context
55.3.18
6
7
8org.springframework.data
9spring-data-redis
102.7.1
11
12
13
14io.lettuce
15lettuce-core
166.1.8.RELEASE
17
利用Lettuce配置Redis连接器有两种方案。第一,创建Lettuce连接工厂,通过连接工厂获取连接; 第二,直接用Lettuce创建Redis连接。
1. Lettuce连接工厂
可以创建一个名为LettuceConfig的类来配置Lettuce连接工厂,代码如文件31所示。
【文件31】LettuceConfig.java
1@Configuration
2public class LettuceConfig {
3@Bean
4public LettuceConnectionFactory redisConnectionFactory() {
5return new LettuceConnectionFactory(
6 new RedisStandaloneConfiguration("127.0.0.1", 6379));
7}
8}
在创建了LettuceConnectionFactory实例后(第5、6行),可以调用该类的getConnection()方法获取Redis连接对象(org.springframework.data.redis.connection.RedisConnection)。同时,可以编写测试类查看获取的连接是否有效。测试代码如文件32所示。
【文件32】TestLettuce.java
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes= LettuceConfig.class)
3public class TestLettuce {
4@Autowired
5private LettuceConnectionFactory factory;
6@Test
7public void testLettuceConnectionFactory(){
8RedisConnection conn = factory.getConnection();
9//向Redis发送 PING 命令,并断言得到 PONG
10Assert.assertEquals("PONG",conn.ping());
11}
12}
如文件32所示,第10行调用RedisConnectionCommands(RedisConnection接口的父接口)接口的ping()方法,用于向Redis发送PING命令(见1.4.2节PING命令部分)。
2. Lettuce直接连接Redis
创建一个名为LettuceConnection的类,代码如文件33所示。
【文件33】LettuceConnection.java
1public class LettuceConnection {
2public static void main(String[] args) {
3//步骤1: 连接信息
4RedisURI redisURI = RedisURI.builder()
5.withHost("127.0.0.1")
6.withPort(6379)
7//.withPassword(new char[]{'a', 'b', 'c'})
8.withTimeout(Duration.ofSeconds(10))
9.build();
10//步骤2: 创建Redis客户端
11RedisClient client = RedisClient.create(redisURI);
12//步骤3: 建立连接
13StatefulRedisConnection connection =
14 client.connect();
15//向Redis发送操作命令,相关代码略
16//关闭连接
17connection.close();
18client.shutdown();
19}
20}
3.2.2Jedis
Jedis是利用Java操作Redis的工具,功能类似于JDBC。Jedis是Redis官方推荐的Java客户端开发包。Jedis支持Redis命令、事务和流水线。配置Jedis连接器,首先要在项目中添加Jedis的依赖,内容如下:
1
2
3redis.clients
4jedis
53.8.0
6
利用Jedis作为Redis连接器有三种实现方案: Jedis连接工厂、Jedis直接连接和Jedis连接池。
1. Jedis连接工厂
创建一个JedisConfig类,代码如文件34所示。
【文件34】JedisConfig.java
1@Configuration
2public class JedisConfig {
3@Bean
4public JedisConnectionFactory redisConnectionFactory() {
5RedisStandaloneConfiguration config =
6new RedisStandaloneConfiguration("127.0.0.1", 6379);
7return new JedisConnectionFactory(config);
8}
9}
如文件34所示,在获取了JedisConnectionFactory实例后(第7行),可以调用该类的getConnection()方法获取Redis连接。
图31Jedis直接连接Redis
2. Jedis直接连接
所谓直接连接是指Jedis在每次发送Redis操作命令前都会新建TCP连接,使用后再断开连接,如图31所示。对于频繁访问Redis的场景这种方案,显然不是高效的使用方式。
利用Jedis直接创建Redis连接非常简单,只需创建Jedis的实例并指定相关参数即可。下述代码描述了图31的完整过程。
1//1.建立连接
2Jedis jedis = new Jedis("localhost",6379);
3//2.发送Redis操作命令
4jedis.set("username","zhangsan");
5//3.返回获取的操作结果
6String username = jedis.get("username");
7//4.关闭连接
8jedis.close();
3. Jedis连接池
在生产环境中,从提升性能的角度考虑,一般使用连接池方式管理Jedis连接。具体做法是将所有的Jedis连接对象预先存放在连接池中,每次连接Redis时,只需要从连接池中借用活动连接,用后再将连接归还给连接池,如图32所示。
图32Jedis连接池
客户端连接Redis使用的协议是TCP。直接连接的方式每次都需要建立TCP连接,而连接池方式是可以预先创建好Redis连接,所以每次只需从连接池中借用即可。而借用和归还操作都是在本地进行的,只有少量的并发同步开销,这个开销远远小于新建TCP连接的开销。另外,直接连接方式无法限制Jedis对象的个数,在极端情况下会造成连接泄露,而连接池方式可以有效地保护和控制资源的使用。表32给出了两种方式各自的优势和劣势。
表32Jedis直接连接方式和连接池方式对比
优点缺点
直接连接简单方便,适用于少量长期连接的场景(1) 存在每次新建、关闭TCP连接的开销;
(2) 资源无法控制,极端情况会出现连接泄露;
(3) Jedis对象线程不安全
连接池(1) 无须每次连接都生成Jedis对象,降低开销;
(2) 保护和控制资源的使用使用相对麻烦,尤其在资源管理上需要很多参数来保证,一旦规划不合理就会出现问题
创建Redis连接池代码如文件35所示。
【文件35】JedisConnectionPool.java
1public class JedisConnectionPool {
2private static final JedisPool jedisPool;
3static{
4 GenericObjectPoolConfig jedisPoolConfig =
5new GenericObjectPoolConfig();
6 //连接池中的最大连接数
7 jedisPoolConfig.setMaxTotal(8);
8 //连接池中的最大空闲连接数
9 jedisPoolConfig.setMaxIdle(8);
10//连接池中的最少空闲连接数
11jedisPoolConfig.setMinIdle(0);
12//当连接资源耗尽后,调用者的最大等待时间,单位: 毫秒。-1表示永不超时
13jedisPoolConfig.setMaxWait(Duration.ofMillis(200));
14jedisPool =
15new JedisPool(jedisPoolConfig,"127.0.0.1",6379,1000);
16}
17//从连接池中借用连接
18public static Jedis getJedis(){
19return jedisPool.getResource();
20}
21}
3.2.3RedisTemplate
目前,Jedis客户端在编程实施方面存在以下一些不足。
(1) 连接管理无法自动化,连接池的设计缺少必要的容器支持。
(2) 数据操作需要关注序列化和反序列化,因为Jedis的客户端API接受的数据类型为String和Byte,对结构化数据(JSON、XML、POJO等)操作需要额外的支持。
(3) 事务操作为硬编码。
(4) 对于发布订阅功能缺乏必要的设计模式支持,对于开发者而言需要关注的内容太多。
RedisTemplate类(org.springframework.data.redis.core.RedisTemplate)是Spring Data Redis中对Jedis API的高度封装。Spring Data Redis相对于Jedis来说可以方便地更换Redis的Java客户端。与Jedis相比,Spring Data Redis进行了如下改进。
(1) 连接池自动管理,提供了一个高度封装的RedisTemplate类。
(2) 针对Jedis客户端大量的API进行了归类封装。遵循Redis命令参考(见Redis官方网站)中的分组,RedisTemplate提供包含丰富的通用子接口的操作视图。这些视图可用于针对特定类型或特定键(通过键绑定接口)进行操作。
(3) 由容器封装并控制事务操作。
(4) 针对数据的序列化和反序列化提供了多种可选择的序列化器(RedisSerializer)。
(5) 基于设计模式和JMS(Java Message Service,Java消息服务)开发思路,将发布订阅的编程接口进行了封装,使开发更加便捷。
(6) RedisTemplate是线程安全的。
本书的后续案例将使用RedisTemplate来实现编程执行Redis操作。要获取RedisTemplate实例,首先要创建连接工厂对象,再借助连接工厂构建RedisTemplate。如果操作的值是String类型,也可以使用RedisTemplate类的子类StringRedisTemplate。由于程序中要频繁使用RedisTemplate对象,可以将其设置为Spring管理的Bean,然后由Spring将该对象注入需要的地方。代码如文件36所示。
【文件36】RedisTemplateConfig.java
1package com.example.redis.template;
2//import部分略
3@Configuration
4public class RedisTemplateConfig {
5@Bean
6publicRedisConnectionFactory redisConnectionFactory() {
7RedisStandaloneConfiguration rsc =
8new RedisStandaloneConfiguration();
9rsc.setHostName("127.0.0.1");
10rsc.setDatabase(0);
11rsc.setPort(6379);
12return new JedisConnectionFactory(rsc);
13}
14@Bean
15public StringRedisTemplate redisTemplate(@Autowired
16RedisConnectionFactory rcf){
17StringRedisTemplate template = new StringRedisTemplate();
18template.setConnectionFactory(rcf);
19return template;
20}
21}
从2.0版本开始,Spring Data Redis已经不推荐直接显式设置连接的信息了,一方面为了使配置信息与建立连接工厂解耦,另一方面抽象出Standalone、Sentinel和RedisCluster三种模式的环境配置类与一个统一的Jedis客户端连接配置类(用于配置连接池和SSL(Secure Socket Layer,安全套接字层)连接),这样可以更加灵活、方便地根据实际业务场景需要来配置连接信息。文件36以Standalone方式为例,展示了在不使用连接池的情况下,如何实例化RedisTemplate。首先创建RedisStandaloneConfiguration实例并设置参数(第7~11行),然后根据该配置实例来初始化Jedis连接工厂(第12行)。
文件36的配置使用的是直接连接Redis的方式,即每次需要时都会创建新的连接。当并发量剧增时,这会带来性能上的开销,同时由于没有对连接数进行限制,可能使服务器崩溃导致无法响应。所以一般会建立连接池,事先初始化一组连接,供需要Redis连接的线程取用。采用连接池方式需要更改文件36的第5~13行,具体如下:
1@Bean
2public RedisConnectionFactory redisConnectionFactory() {
3JedisPoolConfig jpc = new JedisPoolConfig();
4jpc.setMaxTotal(8);
5jpc.setMaxIdle(8);
6jpc.setMinIdle(0);
7jpc.setMaxWait(Duration.ofMillis(200));
8//Redis连接配置
9RedisStandaloneConfiguration redisStandaloneConfiguration=
10new RedisStandaloneConfiguration();
11//设置Redis服务器的IP
12redisStandaloneConfiguration.setHostName("127.0.0.1");
13//设置Redis服务器的端口号
14redisStandaloneConfiguration.setPort(6379);
15//连接的数据库
16redisStandaloneConfiguration.setDatabase(0);
17//JedisConnectionFactory配置jedisPoolConfig
18JedisClientConfiguration.JedisClientConfigurationBuilder
19jedisClientConfiguration = JedisClientConfiguration.builder();
20//指定连接池
21jedisClientConfiguration.usePooling().poolConfig(jpc);
22//创建工厂对象
23RedisConnectionFactory factory=new JedisConnectionFactory(
24 redisStandaloneConfiguration,jedisClientConfiguration.build());
25return factory;
26}
在默认情况下,Redis服务器在启动时会创建16个数据库,编号从0到15。不同的应用可以连接到不同的数据库上。上述代码的第16行通过setDatabase()方法选择编号为0的数据库。此外,Spring Data Redis提供的采用连接池创建RedisTemplate对象的方式并不优雅。如果要采用连接池创建RedisTemplate对象,推荐使用Spring Boot。
RedisTemplate默认使用Java的序列化程序。通过RedisTemplate写入或读取的任何对象都是通过Java序列化和反序列化的。可以通过org.springframework.data.redis.serializer包中提供的接口更改默认的序列化机制的设置,内容可见3.12节。
3.3Spring操作Redis字符串
在创建了RedisTemplate(或StringRedisTemplate)对象后,可以编程执行Redis操作了。Redis可以存取多种不同类型的数据,其中有5种基础数据类型: 字符串、列表、哈希、集合和有序集合。此外,还有流、地理空间数据、位图等。RedisTemplate对基础数据类型的大部分操作都是借助表33中的方法和子接口完成的。
表33RedisTemplate操作基础数据类型的主要方法和子接口
方法子接口描述
opsForValue()ValueOperations操作字符串类型的条目
opsForList()ListOperations操作列表类型的条目
opsForSet()SetOperations操作集合类型的条目
opsForHash()HashOperations操作哈希类型的条目
opsForZSet()ZSetOperations操作有序集合类型的条目
opsForStream()StreamOperations执行流操作命令的接口
opsForHyperLogLog()HyperLogLogOperations操作超级日志
opsForGeo()GeoOperations操作地理空间数据类型的条目
要操作Redis字符串,需要调用RedisTemplate类的opsForValue()方法创建ValueOperations子接口对象,再调用ValueOperations子接口的相关方法。本节介绍ValueOperations子接口(org.springframework.data.redis.core.ValueOperations)中的主要方法的使用。
(1) 方法原型: void set(K key,V value); 功能: 设置键key的值value; 对应Redis命令: SET。
(2) 方法原型: @Nullable V get(Object key); 功能: 返回键key的值,当键的值不存在或在流水线(或事务)中使用该方法时,返回null; 对应Redis命令: GET。
【例31】利用键user保存值hello,redis。可以通过ValueOperations子接口提供的set(String key,String value)方法实现,代码如文件37所示。
【文件37】MyRedisStringTest.java
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes= RedisTemplateConfig.class)
3public class MyRedisStringTest {
4@Autowired
5private StringRedisTemplate redis;
6@Test
7public void testEx1() {
8redis.opsForValue().set("user","hello,redis");
9String str = redis.opsForValue().get("user");
10assertEquals("hello,redis",str);
11}
12}
如文件37所示,本节的案例由于操作的值都是String类型的,因此采用了RedisTemplate类的子类StringRedisTemplate。StringRedisTemplate对象在文件36的第14~20行完成实例化,并注入测试类中(第4、5行)。表33中的子接口对象可以通过RedisTemplate或StringRedisTemplate创建。即通过调用RedisTemplate的opsForValue()方法获得子接口引用(第8行),再调用子接口中对应的方法完成字符串操作。第8行调用set()方法向Redis中以user为键,写入值hello,redis。第9行通过键user取出对应的值。
(3) 方法原型: void set(K key,V value,long timeout,TimeUnit unit); 功能: 设置键key的值value和过期超时时长timeout; 对应Redis命令: SETEX。
【例32】以name为键将值Tom写入Redis,并设置写入键值的有效时长为10秒。测试代码如文件38所示。
【文件38】例32测试代码
1@Test
2public void testEx2() throws Interrupted Exception {
3 redis.opsForValue().set("name","Tom",10,TimeUnit.SECONDS);
4 Thread.sleep(11000);
5 String str = redis.opsForValue().get("name");
6 assertEquals("Tom",str);
7}
图33例32测试代码运行结果
在设置name键值后,等待10秒再去获取name键的值,get()方法将返回null,测试代码运行结果如图33所示。
(4) 方法原型: void set(K key,V value,long offset); 功能: 对于键key所存储的字符串; 从指定偏移量offset开始用给定值value替代对应Redis命令: SETRANGE。
【例33】向Redis中以key为键存入字符串hello,world,随后将该字符串替换为hello,redis。测试代码如文件39所示。
【文件39】例33测试代码
1@Test
2public void testEx3(){
3redis.opsForValue().set("key","hello,world");
4redis.opsForValue().set("key","redis",6);
5String str = redis.opsForValue().get("key");
6assertEquals("hello,redis",str);
7}
(5) 方法原型: @Nullable Boolean setIfAbsent(K key,V value); 功能: 如果缺少键key,则设置以键key保存字符串值value; 对应的Redis命令: SETNX。
【例34】以abs为键,调用setIfAbsent()方法保存字符串absent,保存后将值改为present。测试代码如文件310所示。
【文件310】例34测试代码
1@Test
2public void testEx4(){
3assertTrue(redis.opsForValue().setIfAbsent("abs","absent"));
4assertFalse(redis.opsForValue().setIfAbsent("abs","present"));
5}
如文件310所示,如果Redis中不存在键abs,则第3行代码执行成功。第4行准备将键abs的值修改为present。由于键abs已存在,因此此次更改失败。
(6) 方法原型: void multiSet(Map extends K,? extends V>map); 功能: 使用元组中提供的键值对将多个键设置为多个值; 对应的Redis命令: MSET。
(7) 方法原型: @Nullable ListmultiGet(Collectionkeys); 功能: 返回所有给定键的值,值按键的请求顺序返回; 对应的Redis命令: MGET。
【例35】将字符串aaa、bbb和ccc一次性存入Redis,这三个字符串对应的键分别为multi1、multi2和multi3,再通过各自的键将它们取出。测试代码如文件311所示。
【文件311】例35测试代码
1@Test
2public void testEx5(){
3Map map = new HashMap();
4map.put("multi1","aaa");
5map.put("multi2","bbb");
6map.put("multi3","ccc");
7redis.opsForValue().multiSet(map);
8
9List list = new ArrayList();
10list.add("multi1");
11list.add("multi2");
12list.add("multi3");
13List values = redis.opsForValue().multiGet(list);
14values.forEach(System.out::println);
15}
(8) 方法原型: @Nullable V getAndSet(K key,V value); 功能: 设置键key的值并返回其旧值; 对应的Redis命令: GETSET。
【例36】应用getAndSet()方法。测试代码如文件312所示。
【文件312】例36测试代码
1@Test
2public void testEx6(){
3 redis.opsForValue().set("getset","test-11");
4 String str = redis.opsForValue().getAndSet("getset","test-22");
5 assertEquals("test-11",str);
6}
(9) 方法原型: @Nullable Long increment(K key,long delta); 功能: 将存储在键key下的字符串的整数值按增量delta递增,如果key指定的值不存在,那么key的值会先被初始化为0,然后再执行递增; 对应的Redis命令: INCRBY。
【例37】点赞是社交网络中最常用的功能。本例模拟社交网络中对作品的点赞功能,并输出当前作品获赞的数量。测试代码如文件313所示。
【文件313】例37测试代码
1@Test
2public void testEx7(){
3boolean laud_flag = true;
4Long l = 0L;
5if(laud_flag)
6l = redis.opsForValue().increment("articleId",1);
7else
8l = redis.opsForValue().increment("articleId",-1);
9System.out.println(l);
10}
将点赞数量存入Redis,以作品Id(标识符属性,字符串类型)为键。如果用户点赞,则变量laud_flag取值为true,对应的点赞数增加1; 反之,用户取消点赞,变量laud_flag取值为false,对应的点赞数减1。
(10) 方法原型: @Nullable Boolean setBit(K key,long offset,boolean value); 功能: 对键key所存储的字符串值,设置或清除指定偏移量上的位; 对应的Redis命令: SETBIT。
(11) 方法原型: @Nullable Boolean getBit(K key,long offset); 功能: 获取键对应值的ASCII码在offsest处的值; 对应的Redis命令: GETBIT。
【例38】利用键bit存入字符串a,并利用位运算将该字符串改为b。测试代码如文件314所示。
【文件314】例38测试代码
1@Test
2public void testEx8(){
3redis.opsForValue().set("bit","a");
4assertTrue(redis.opsForValue().getBit("bit",7));
5redis.opsForValue().setBit("bit",6,true);
6redis.opsForValue().setBit("bit",7,false);
7assertEquals("b",redis.opsForValue().get("bit"));
8}
上述测试代码第3行以bit为键存入字符串a。字符'a'的ASCII码是97,其二进制形式为01100001。这样,第4行获取第7位的ASCII码值,得到二进制1。1代表true,0代表false。因此,第4行结果应为true。第5、6行分别将字符'a'的ASCII码的第6、7位设置为1和0,这样键bit对应的ASCII码被改为01100010,即字符'b'的ASCII码。
目前的软件系统(包括电商和社交网络)经常有这样的需求: 根据用户提供的手机号码发送验证码,实现登录。下面的案例模拟实现手机验证码的发送功能。
【例39】利用Redis实现模拟手机验证码登录之验证码发送功能。对于验证码的发送,通常有如下要求:
(1) 发送的手机验证码几分钟内(本例设定为1分钟)有效。
(2) 每天向每个手机号码发送的验证码的次数有限(本例设定为24小时内最多发送3次)。
为实现上述两项要求,可以利用Redis保存两个值,并设置它们的生存时间: 一个用于保存发送给用户的验证码,生存时长为1分钟; 另一个用来保存给用户发送验证码的次数,生存时间为24小时。测试代码如文件315所示。
【文件315】ShortMessageSender.java
1public class ShortMessageSender {
2public String sendCode(StringRedisTemplate template,String phone){
3String codeKey = phone+"_CODE";
4String countKey = phone +"_COUNT";
5Integer count = 0;
6try {
7count = Integer.parseInt(
8template.opsForValue().get(countKey));
9} catch(NumberFormatException nfe) {
10count = 0;
11}
12if(count>2) {
13 System.out.println("24小时内发送次数已达3次,24小时后重试");
14 return "retry";
15}
16Boolean hasCodeKey = template.hasKey(codeKey);
17if(hasCodeKey){
18 Long codeTTL = template.getExpire(codeKey);
19 System.out.println("验证码1分钟内有效,请在"+codeTTL+
20"秒之后再次发送");
21 return "tip";
22}
23 String code = RandomUtil.randomNumbers(6);
24 System.out.println("CODE IS : "+code);
25 template.opsForValue().set(codeKey,code,60, TimeUnit.SECONDS);
26 long timeout = 24*60*60;
27 if(count!=0)
28timeout = template.getExpire(countKey);
29template.opsForValue().set(countKey,String.valueOf(count+1),
30timeout,TimeUnit.SECONDS);
31System.out.println("验证码发送成功");
32return "success";
33}
34}
如文件315所示,对于用户提交的手机号码,以参数phone传入sendCode()方法,并省去了对手机号码合法性的校验。第3、4行分别以手机号码附带后缀的形式定义了两个键,这两个键对应的值分别为验证码和发送验证码的次数。第7、8行试图从Redis中获取发送验证码的次数,当Redis中不存在键countKey时,将记录验证码发送次数的变量count计为0(第9~11行)。第23行采用RandomUtil工具随机生成包含6位数字的验证码。要使用RandomUtil工具,需要在pom.xml文件中添加相关依赖:
1
2
3cn.hutool
4hutool-all
55.8.20
6
第25行将验证码保存到Redis,以便用户提交验证码后进行比对。同时,设置了验证码在Redis中的保存时间为60秒。第29行用同样的方法将验证码的发送次数保存到Redis,同时设置该值在Redis中的保存时间为24小时。
随后,模拟向号码为15612345678的手机发送验证码,测试代码如文件316所示。
【文件316】TestSMSVerification.java
1@RunWith(SpringJUnit4ClassRunner.class)
2@ContextConfiguration(classes= RedisTemplateConfig.class)
3public class TestSMSVerification {
4@Autowired
5private StringRedisTemplate template;
6
7@Test
8public void testSender(){
9ShortMessageSender sender = new ShortMessageSender();
10sender.sendCode(template,"15612345678");
11}
12}
测试代码运行结果如图34所示,再次运行测试代码,结果如图35所示。
图34文件316测试代码运行结果
图35再次运行测试代码的结果
3.4Spring操作Redis列表
在Redis中,列表类型是按照元素插入的顺序排序的字符串列表。可以向列表的头部(左边)或尾部(右边)添加、删除元素。操作Redis列表的方法与操作Redis字符串的方法类似,要调用RedisTemplate类的opsForList()方法创建ListOperations子接口对象,再调用ListOperations子接口的相关方法。本节介绍ListOperations子接口(org.springframework.data.redis.core.ListOperations)中的主要方法的使用。
(1) 方法原型: @Nullable Long leftPush(K key,V value); 功能: 将一个或多个值value插入列表key的表头,特点是先进后出,可以作为栈使用; 返回值: 执行插入操作后的列表长度; 对应的Redis命令: LPUSH。
(2) 方法原型: @Nullable Long rightPush(K key,V value); 功能: 将一个或多个值value插入列表key的表尾,特点是先进先出,可以作为队列使用; 返回值: 执行插入操作后的列表长度; 对应的Redis命令: RPUSH。
(3) 方法原型: @Nullable Listrange(K key,long start,long end); 功能: 获取指定范围内的元素列表,参数start和end分别代表开始索引和结束索引。索引从左到右分别为0到N-1,从右到左为-1到-N。并且,end参数包含了自身。对应的Redis命令: LRANGE。
【例310】将数字1~10以strs为键保存到Redis中,并倒序输出。测试代码如文件317所示。
【文件317】例310测试代码
1@Test
2public void testEx9() {
3for(int i=1;i<11;i++)
4redis.opsForList().leftPush("strs",String.valueOf(i));
5redis.opsForList().range("strs", 0, -1)
6.forEach(e -> System.out.print(e+" "));
7}
图36例310测试代码
运行结果
执行测试代码,运行结果如图36所示。从控制台输出可见leftPush()方法实现了栈操作。读者可自行修改代码,实现正序输出。
(4) 方法原型: @Nullable Long rightPushAll(K key,Collectionvalues); 功能: 将一组值插入列表key的尾部; 返回值: 执行插入后的列表长度; 对应的Redis命令: RPUSH。
(5) 方法原型: @Nullable Long leftPushAll(K key,Collectionvalues); 功能: 将一组值插入列表key的头部; 返回值: 执行插入后的列表长度; 对应的Redis命令: LPUSH。
【例311】将数字1~10以strs为键批量保存到Redis中,并正序输出。测试代码如文件318所示。
【文件318】例311测试代码
1@Test
2public void testEx10() {
3List strs = new ArrayList();
4for(int i=1;i<11;i++)
5strs.add(String.valueOf(i));
6redis.opsForList().rightPushAll("strs",strs);
7redis.opsForList().range("strs", 0, -1)
8.forEach(System.out::println);
9}
(6) 方法原型: void trim(K key,long start,long end); 功能: 修剪列表,使其保留start到end之间的值; 对应的Redis命令: LTRIM。
【例312】修剪例310中的strs列表,保留6~10。测试代码如文件319所示。
【文件319】例312测试代码
1@Test
2public void testEx11() {
3System.out.println(redis.opsForList().range("strs",0,-1));
4redis.opsForList().trim("strs",5,-1);
5System.out.println(redis.opsForList().range("strs",0,-1));
6}
图37例312测试代码运行结果
测试代码运行结果如图37所示。
(7) 方法原型: @Nullable V leftPop(K key); 功能: 移除并返回列表key中的第一个元素; 对应的Redis命令: LPOP。
(8) 方法原型: @Nullable V rightPop(K key); 功能: 移除并返回列表key中的最后一个元素; 对应的Redis命令: RPOP。
【例313】利用Redis List数据类型实现栈和队列。测试代码如文件320所示。
【文件320】例313测试代码
1@Test
2public void testEx12(){
3String s = "abcde";
4for(int i=0;i0:从表头开始向表尾搜索,移除值与value相等的元素,数量为count。
② count<0:从表尾开始向表头搜索,移除值与value相等的元素,数量为count的绝对值。
③ count=0:移除表中所有值与value相等的值。
④ 返回值: 被移除的元素的数量。因为不存在的key被视作空表,所以当key不存在时,该方法返回0。对应的Redis命令: LREM。
【例317】移除列表中的重复值。测试代码如文件324所示。
【文件324】例317测试代码
1@Test
2public void testEx16() {
3String[] s = {"hello","hello","foo","hello"};
4redis.opsForList().rightPushAll("lrm",s);
5System.out.println(redis.opsForList().range("lrm",0,-1));
6redis.opsForList().remove("lrm",-2,"hello");
7System.out.println(redis.opsForList().range("lrm",0,-1));
8}
图311例317测试代码
运行结果
运行此测试代码,结果如图311所示。
社交网络(或电商系统)中经常有这样的需求,用户可以查看浏览内容的历史记录。如果只是要求保留用户的浏览记录,则可以用列表来实现。
【例318】Id为101的用户某时段的浏览记录为{a.html,b.html,...,g.html},要求将用户最近的5条浏览记录保留3天。测试代码如文件325所示。
【文件325】例318测试代码
1@Test
2public void testViewed(){
3String[] pages = {"a.html","b.html","c.html","d.html","e.html",
4"f.html","g.html"};
5 //保留最近5条浏览记录
6int viewed_page_counter = 5;
7int offset = pages.length-viewed_page_counter;
8for(int i=0;i viewedPages = template.opsForList().range(
22"101:20241011:viewed",0,-1);
23viewedPages.forEach(System.out::println);
24}
测试代码运行结果如图312所示。
图312例318测试代码运行结果
3.5Spring操作Redis哈希
几乎所有的编程语言都提供了哈希类型。Redis的哈希类型值是一个键值对结构,形如value={{field1,value1},...,{fieldn,valuen}},因此哈希类型特别适合存储对象。Redis是以键值对的形式存储数据的。Redis键值对和哈希类型二者的关系可以用图313表示。
图313Redis键值对和哈希
类型的关系
如图313所示,普通哈希类型数据与以键user:1存储在Redis中。其映射关系在Redis中叫作字段值(fieldvalue),注意这里的值是指字段(field)对应的值,不是键对应的值。操作Redis哈希的方法与操作Redis字符串的方法类似,要调用RedisTemplate类的opsForHash()方法创建HashOperations子接口对象,再调用HashOperations子接口的相关方法。本节介绍HashOperations子接口(org.springframework.data.redis.core.HashOperations)中的主要方法的使用。
(1) 方法原型: void put(H key,HK hashKey,HV value); 功能: 将哈希key中的字段hashKey的值设置为value; 对应的Redis命令: HSET。
(2) 方法原型: @Nullable HV get(H key,Object hashKey); 功能: 从哈希key中根据字段hashKey取出值; 对应的Redis命令: HGET。
(3) 方法原型: Mapentries(H key); 功能: 根据key获取整个哈希存储的值; 对应的Redis命令: HGETALL。
(4) 方法原型: Setkeys(H key); 功能: 获取哈希key的所有字段名的集合; 对应的Redis命令: HKEYS。
(5) 方法原型: Listvalues(H key); 功能: 获取哈希key的所有字段的值; 对应的Redis命令: HVALS。
(6) 方法原型: Long size(H key); 功能: 返回哈希中字段的数量; 对应的Redis命令: HLEN。
(7) 方法原型: Boolean hasKey(H key,Object hashKey); 功能: 判断哈希key中给定的字段hashKey是否存在; 对应的Redis命令: HEXISTS。
【例319】Redis哈希基础操作1。要求: ①将哈希数据、、存储到哈希rHash中; ②返回rHash中字段的数量; ③取出年龄值; ④取出rHash中存储的全部值; ⑤取出全部字段名; ⑥取出全部字段值; ⑦判断rHash中是否存在age字段和ttt字段。测试代码如文件326所示。
【文件326】例319测试代码
1@Test
2public void testEx17() {
3//①
4template.opsForHash().put("rHash", "name", "Tom");
5template.opsForHash().put("rHash", "age", "26");
6template.opsForHash().put("rHash", "class", "6");
7//②
8assertEquals(3, template.opsForHash().size("rHash").intValue());
9//③
10 assertEquals("26", template.opsForHash().get("rHash", "age"));
11//④
12System.out.println("stored Hash:" +
13 template.opsForHash().entries("rHash"));
14//⑤
15System.out.println("fields:" +
16 template.opsForHash().keys("rHash"));
17//⑥
18System.out.println("values:" +
19template.opsForHash().values("rHash"));
20//⑦
21assertTrue(template.opsForHash().hasKey("rHash", "age"));
22assertFalse(template.opsForHash().hasKey("rHash", "ttt"));
23}
图314例319测试代码运行结果
运行上述测试代码,结果如图314所示。
(8) 方法原型: Cursor>scan(H key,ScanOptions options); 功能: 用于增量迭代哈希key中的键值对,参数ScanOptions是用于SCAN命令的选项,目前的常量值为NONE(对扫描模式不做限制); 对应的Redis命令: HSCAN。
该命令支持增量迭代,即每次执行都只会返回少量元素,所以该命令可以用于生产环境,而不会出现像KEYS命令带来的问题: 当KEYS命令被用于处理一个大的数据库时,可能会阻塞服务器达数秒之久。在对键进行增量式迭代的过程中,键可能会被修改,所以增量式迭代命令只能对被返回的元素提供有限的保证。
(9) 方法原型: Long increment(H key,HK hashKey,long delta); 功能: 为哈希key中的字段hashKey的值加上增量delta。增量也可以为负数,相当于对给定字段进行减法操作。如果key不存在,则会创建一个新的哈希并执行该方法。如果域hashKey不存在,那么在执行该方法前,字段的值被初始化为0。对一个存储字符串值的字段执行该方法将造成一个错误。对应的Redis命令: HINCRBY。
由于内部序列化器的设置不同,因此此方法要求配合StringRedisTemplate类使用。
(10) 方法原型: void putAll(H key,Mapm); 功能: 将多个字段值对同时设置到哈希key中。执行此方法会覆盖哈希中已存在的字段。如果key不存在,则创建一个空的哈希并执行该方法; 对应的Redis命令: HMSET。
(11) 方法原型: ListmultiGet(H key,CollectionhashKeys); 功能: 返回哈希key中,一个或多个给定字段hashKeys的值。如果给定的字段不存在于哈希,则返回一个nil值。对应的Redis命令: HMGET。
(12) 方法原型: Long delete(H key,Object...hashKeys); 功能: 删除哈希key中一个或多个指定的字段hashKeys。对应的Redis命令: HDEL。
【例320】Redis哈希基础操作2。要求: ①使用scan()方法遍历例319中的哈希rHash,并输出其全部值; ②将age字段的值增加1; ③将哈希数据、、一次性加入哈希rHash2中; ④从哈希rHash中取出name字段和age字段的值; ⑤删除哈希rHash中的字段name。测试代码如文件327所示。
【文件327】例320测试代码
1@Test
2public void testEx18 () {
3//①
4template.opsForHash().put("rHash", "name", "Tom");
5template.opsForHash().put("rHash", "age", "26");
6template.opsForHash().put("rHash", "class", "6");
7System.out.println("all the values in rHash are:");
8Cursor> cursor =
9template.opsForHash().scan("rHash", ScanOptions.NONE);
10cursor.forEachRemaining(entry -> System.out.println(
11entry.getKey()+":"+entry.getValue()));
12//②
13//需注入StringRedisTemplate
14assertEquals("27",template.opsForHash().increment(
15"rHash","age",1).toString());
16//③
17Map tempMap = new HashMap();
18tempMap.put("name","Bob");
19tempMap.put("age","28");
20tempMap.put("class","2");
21template.opsForHash().putAll("rHash2",tempMap);
22System.out.println("rHash:"
23+template.opsForHash().entries("rHash"));
24System.out.println("rHash2:"
25+template.opsForHash().entries("rHash2"));
26//④
27List