Spring-data-redis 的使用实践

来自Wikioe
跳到导航 跳到搜索


关于

无论是 Jedis 还是 JedisPool,都只是完成对 Redis 操作的极为基础的 API,在不依赖任何中间件的开发环境中,可以使用它们。但是,一般的 Java 开发,都会使用了 Spring 框架,可以使用 spring-data-redis 开源库来简化 Redis 操作的代码逻辑,做到最大程度的业务聚焦。


CRUD 中应用缓存的场景

在普通 CRUD 应用场景中,很多情况下需要同步操作缓存,推荐使用 Spring 的 spring-data-redis 开源库。

  • 注:CRUD 是指 Create(创建),Retrieve(查询),Update(更新)和 Delete(删除)。
  1. 创建缓存
    在创建(Create)一个 POJO 实例的时候,对 POJO 实例进行分布式缓存,一般以“缓存前缀+ID”为缓存的 Key 键,POJO 对象为缓存的 Value 值,直接缓存 POJO 的二进制字节。
    • 前提是:POJO 必须可序列化,实现 java.io.Serializable 空接口。如果 POJO 不可序列化,也是可以缓存的,但是必须自己实现序列化的方式,例如使用 JSON 方式序列化。
  2. 查询缓存
    在查询(Retrieve)一个 POJO 实例的时候,首先应该根据 POJO 缓存的 Key 键,从 Redis 缓存中返回结果。
    • 如果不存在,才去查询数据库,并且能够将数据库的结果缓存起来。
  3. 更新缓存
    在更新(Update)一个 POJO 实例的时候,既需要更新数据库的 POJO 数据记录,也需要更新 POJO 的缓存记录。
  4. 删除缓存
    在删除(Delete)一个 POJO 实例的时候,既需要删除数据库的 POJO 数据记录,也需要删除 POJO 的缓存记录。


为了演示 CRUD 场景下 Redis 的缓存操作

首先定义一个简单的 POJO 实体类:聊天系统的用户类。
此类拥有一些简单的属性,例如 uid 和 nickName,且这些属性都具备基本的 getter 和 setter 方法:
package com.crazymakercircle.im.common.bean;
//...
import java.io.Serializable;
@Slf4j
public class User implements Serializable {
	String uid;
	String devId;
	String token;
	String nickName;
	//....省略 getter setter toString等方法
}
然后定义一个完成 CRUD 操作的 Service 接口:UserService,定义三个方法:
  1. saveUser完成创建(C)、更新操作(U)。
  2. getUser完成查询操作(R)。
  3. deleteUser完成删除操作(D)。
Service接口的代码如下:
package com.crazymakercircle.redis.springJedis;
import com.crazymakercircle.im.common.bean.User;
public interface UserService {
	/**
	* CRUD 的创建/更新
	* @param user 用户
	*/
	User saveUser(final User user);
    
	/**
	* CRUD 的查询
	* @param id id
	* @return 用户
	*/
	User getUser(long id);
    
	/**
	* CRUD 的删除
	* @param id id
	*/
	void deleteUser(long id);
}

定义完了 Service 接口之后,接下来就是定义 Service 服务的具体实现。不过,这里聚焦的是:如何通过 spring-data-redis 库,使 Service 实现带缓存的功能?

配置 spring-redis.xml

使用 spring-data-redis 库的步骤:

  1. 配置依赖
    在 Maven 的 pom 文件中加上 spring-data-redis 库的依赖,具体如下:
    <dependency>
    	<groupId>org.springframework.data</groupId>
    	<artifactId>spring-data-redis</artifactId>
    	<version>${springboot}</version>
    </dependency>
    
  2. 配置 spring-redis.xml
    连接池实例、RedisTemplate模板实例;(均为 spring bean)
    • 这是两个 spring bean,可以配置在项目统一的 spring xml 配置文件中,也可以编写一个独立的 springredis.xml 配置文件。
      (以下采用第二种方式:“spring-redis.xml”)
    <!--加载配置文件 -->
    <context:property-placeholder location="classpath:redis.properties"/>
    
    <!--redis 数据源 -->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
    	<!--最大空闲数 -->
    	<property name="maxIdle" value="${redis.maxIdle}"/>
    	<!--最大空连接数 -->
    	<property name="maxTotal" value="${redis.maxTotal}"/>
    	<!--最大等待时间 -->
    	<property name="maxWaitMillis" value="${redis.maxWaitMillis}"/>
    	<!--连接超时的时候是否阻塞,true表示阻塞,直到超过maxWaitMillis, 默认为true -->
    	<property name="blockWhenExhausted" value="${redis.blockWhenExhausted}"/>
    	<!--获取连接时,检测连接是否成功 -->
    	<property name="testOnBorrow" value="${redis.testOnBorrow}"/>
    </bean>
    
    <!-- Spring-redis 连接池管理工厂 -->
    <bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
    	<!-- IP地址 -->
    	<property name="hostName" value="${redis.host}"/>
    	<!--端口号 -->
    	<property name="port" value="${redis.port}"/>
    	<!--连接池配置引用 -->
    	<property name="poolConfig" ref="poolConfig"/>
    	<!--usePool:是否使用连接池 -->
    	<property name="usePool" value="true"/>
    </bean>
    
    <!-- redis template definition -->
    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
    	<property name="connectionFactory" ref="jedisConnectionFactory"/>
    	
    	<property name="keySerializer">
    		<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    	</property>
    	<property name="valueSerializer">
    		<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
    	</property>
    	<property name="hashKeySerializer">
    		<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
    	</property>
    	<property name="hashValueSerializer">
    		<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
    	</property>
    	
    	<!--开启事务 -->
    	<property name="enableTransactionSupport" value="true"></property>
    </bean>
    
    <!-- 省略其他的 spring-redis.xml 配置 -->
    


spring-data-redis 库在 JedisPool 提供连接池的基础上封装了自己的连接池—— RedisConnectionFactory 连接工厂;并且 spring-data-redis 封装了一个短期、非线程安全的连接类,名为 RedisConnection 连接类。

RedisConnection 类和 Jedis 库中的 Jedis 类原理一样,提供了与 Redis 客户端命令一对一的 API 函数,用于操作远程 Redis 服务。
在使用 spring-data-redis 时,虽然没有直接用到 Jedis 库,但是 spring-data-redis 库底层对 Redis 服务的操作还是调用 Jedis 库完成的。也就是说,spring-data-redis 库从一定程度上使大家更好地使用 Jedis 库。
RedisConnection 的 API 命令操作的对象都是字节级别的 Key 键和 Value 值。为了更进一步地减少开发的工作,spring-data-redis 库在 RedisConnection 连接类的基础上,针对不同的缓存类型,设计了五大数据类型的命令 API 集合,用于完成不同类型的数据缓存操作,并封装在 RedisTemplate 模板类中。

使用 RedisTemplate 模板 API

RedisTemplate 模板类位于核心包 org.springframework.data.redis.core 中,它封装了五大数据类型的命令 API 集合:

  1. ValueOperations:字符串类型操作 API 集合。
  2. ListOperations:列表类型操作 API 集合。
  3. SetOperations:集合类型操作 API 集合。
  4. ZSetOperations:有序集合类型 API 集合。
  5. HashOperations:哈希类型操作 API 集合。
  • 每一种类型的操作 API 基本上都和每一种类型的Redis客户端命令一一对应。但是在 API 的名称上并不完全一致,RedisTemplate 的 API 名称更加人性化。
    例如,Redis 客户端命令“setNX”——Key-Value不存在才设值,在 RedisTemplate 的 API 名称为“setIfAbsent”。


在代码中,除了 key 相关的 Redis 操作(如:keys、hasKey)直接使用 redisTemplate 实例完成。其他的 API 命令,都是在不同类型的命令集合类上完成。

RedisTemplate 提供了5个方法,取得不同类型的命令集合,具体为:

  1. redisTemplate.opsForValue():取得 String 类型命令API集合。
  2. redisTemplate.opsForList():取得 List 类型命令API集合。
  3. redisTemplate.opsForSet():取得 Set 类型命令API集合。
  4. redisTemplate.opsForHash():取得 Hash 类型命令API集合。
  5. redisTemplate.opsForZSet():取得 Zset 类型命令API集合。

然后,在不同类型的命令 API 集合上,使用各种数据类型特有的 API 函数,完成具体的 Redis API 操作。


在实际开发中,为了尽可能地减少第三方库的“入侵”,或者为了在不同的第三方库之间进行方便的切换,一般来说,要对第三方库进行封装。

示例:

  1. 将 RedisTemplate 模板类的大部分缓存操作封装成一个自己的缓存操作 Service 服务 —— CacheOperationService,部分源代码节选如下:
    package com.crazymakercircle.redis.springJedis;
    //...
    public class CacheOperationService {
    	private RedisTemplate redisTemplate;
    	
    	public void setRedisTemplate(RedisTemplate redisTemplate) {
    		this.redisTemplate = redisTemplate;
    	}
    	
    	
    	// --------------RedisTemplate基础操作 --------------------
    	/**
    	* 取得指定格式的所有的key键
    	*
    	* @param patens 匹配的表达式
    	* @return key 的集合
    	*/
    	public Set getKeys(Object patens) {
    		try {
    			return redisTemplate.keys(patens);
    		} catch (Exception e) {
    			e.printStackTrace();
    			return null;
    		}
    	}
    	
    	/**
    	* 指定缓存失效的时间
    	*
    	* @param key键
    	* @param time 时间(秒)
    	* @return
    	*/
    	public boolean expire(String key, long time) {
    		try {
    			if (time > 0) {
    				redisTemplate.expire(key, time, TimeUnit.SECONDS);
    			}
    			return true;
    		} catch (Exception e) {
    			e.printStackTrace();
    			return false;
    		}
    	}
    	
    	/**
    	* 根据key 获取过期的时间
    	* @param key 键不能为null
    	* @return 时间(秒) 返回0代表为永久有效
    	*/
    	public long getExpire(String key) {
    		return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    	}
    	
    	/**
    	* 判断key是否存在
    	* @param key 键
    	* @return true则存在,false则不存在
    	*/
    	public boolean hasKey(String key) {
    		try {
    			return redisTemplate.hasKey(key);
    		} catch (Exception e) {
    			e.printStackTrace();
    			return false;
    		}
    	}
    	
    	/**
    	* 删除缓存
    	* @param key 可以传一个值或多个
    	* @return 删除的个数
    	*/
    	public void del(String... key) {
    		if (key != null &&key.length> 0) {
    			if (key.length == 1) {
    				redisTemplate.delete(key[0]);
    			} else {
    				redisTemplate.delete(CollectionUtils.arrayToList(key));
    			}
    		}
    	}
    	
    	
    	// --------------RedisTemplate操作 String字符串 --------------------
    	/**
    	* 获取String
    	* @param key 键
    	* @return 值
    	*/
    	public Object get(String key) {
    		return key == null ? null : redisTemplate.opsForValue().get(key);
    	}
    	
    	/**
    	* 设置String
    	* @param key 键
    	* @param value 值
    	* @return true则成功,false则失败
    	*/
    	public boolean set(String key, Object value) {
    		try {
    			redisTemplate.opsForValue().set(key, value);
    			return true;
    		} catch (Exception e) {
    			e.printStackTrace();
    			return false;
    		}
    	}
    	
    	//...省略其他的String 操作,请参见源代码
    	
    	
    	// --------------RedisTemplate操作list列表 --------------------
    	/**
    	* 获取list缓存的内容,start == 0 到 end == -1代表所有值
    	* @param key 键
    	* @param start开始,从0开始
    	* @param end结束
    	* @return
    	*/
    	public List<Object> lGet(String key, long start, long end) {
    		try {
    			return redisTemplate.opsForList().range(key, start, end);
    		} catch (Exception e) {
    			e.printStackTrace();
    			return null;
    		}
    	}
    	
    	/**
    	* 将list放入缓存,从右边(后端)插入
    	* @param key 键
    	* @param value 值
    	* @return
    	*/
    	public boolean lSet(String key, Object value) {
    		try {
    			redisTemplate.opsForList().rightPush(key, value);
    			return true;
    		} catch (Exception e) {
    			e.printStackTrace();
    			return false;
    		}
    	}
    	
    	//...省略其他的list操作,请参见源代码
    	
    	
    	// --------------RedisTemplate操作 Hash列表 --------------------
    	/**
    	* HashGet,设置map中的一个field
    	* @param key 键不能为null
    	* @param field项不能为null
    	* @return 值
    	*/
    	public Object hget(String key, String field) {
    		return redisTemplate.opsForHash().get(key, field);
    	}
    	
    	/**
    	* HashSet
    	* @param key 键
    	* @param map 对应多个键值
    	* @return true 成功 false 失败
    	*/
    	public boolean hmset(String key, Map<String, Object> map) {
    		try {
    			redisTemplate.opsForHash().putAll(key, map);
    			return true;
    		} catch (Exception e) {
    			e.printStackTrace();
    			return false;
    		}
    	}
    	
    	//...省略其他的hash操作,请参见源代码
    	
    	
    	// --------------RedisTemplate操作 Set集合 --------------------
    	/**
    	* 根据key获取Set中的所有值
    	*
    	* @param key 键
    	* @return
    	*/
    	public Set<Object> sGet(String key) {
    		try {
    			return redisTemplate.opsForSet().members(key);
    		} catch (Exception e) {
    			e.printStackTrace();
    			return null;
    		}
    	}
    	
    	/**
    	* 将数据放入set缓存
    	* @param key 键
    	* @param values 值可以是多个
    	* @return 成功个数
    	*/
    	public long sSet(String key, Object... values) {
    		try {
    			return redisTemplate.opsForSet().add(key, values);
    		} catch (Exception e) {
    			e.printStackTrace();
    			return 0;
    		}
    	}
    	
    	//...省略其他的Set操作,请参见源代码
    }
    
  2. 配置“spring-redis.xml”:将 redisTemplate 封装成通用服务(为缓存 Service 配置依赖)
    <!-- 省略其他的spring-redis.xml配置,具体参见源代码 -->
    <!-- 1、redis 数据源
    	 2、Spring-redis 连接池管理工厂
    	 3、redis template definition
    -->
    <!-- . . . -->
    
    
    <!--将 redisTemplate 封装成通用服务-->
    <bean id="springRedisService" class="com.crazymakercircle.redis.springJedis.CacheOperationService">
    	<property name="redisTemplate" ref="redisTemplate"/>
    </bean>
    

使用 RedisTemplate 模板 API 完成 CRUD 的实践案例

封装完成了自己的 CacheOperationService 缓存管理服务之后,可以注入到 Spring 的业务 Service 中,就可以完成缓存的 CRUD 操作了。

  • 使用 CacheOperationService 后,就能非常方便地进行缓存的管理,同时,在进行 POJO 的查询时,能优先使用缓存数据,省去了数据库访问的时间。


示例:

  1. 实现业务类 UserServiceImplWithTemplate 类(实现“UserService”接口,包含“CacheOperationService”成员对象),用于完成 User 实例缓存的 CRUD。
    package com.crazymakercircle.redis.springJedis;
    import com.crazymakercircle.im.common.bean.User;
    import com.crazymakercircle.util.Logger;
    public class UserServiceImplWithTemplate implements UserService {
    	public static final String USER_UID_PREFIX = "user:uid:";
    	protected CacheOperationService cacheOperationService;
    	private static final long CASHE_LONG = 60 * 4;//4分钟
    	
    	public void setCacheOperationService(CacheOperationService cacheOperationService) {
    		this.cacheOperationService = cacheOperationService;
    	}
    	
    	
    	/**
    	* CRUD 的创建/更新
    	* @param user 用户
    	*/
    	@Override
    	public User saveUser(final User user) {
    		//保存到缓存
    		String key = USER_UID_PREFIX + user.getUid();
    		Logger.info("user :", user);
    		cacheOperationService.set(key, user, CASHE_LONG);
    		//保存到数据库
    		//...如mysql
    		return user;
    	}
    	
    	/**
    	* CRUD 的查询
    	* @param id id
    	* @return 用户
    	*/
    	@Override
    	public User getUser(final long id) {
    		//首先从缓存中获取
    		String key = USER_UID_PREFIX + id;
    		User value = (User) cacheOperationService.get(key);
    		if (null == value) {
    			//如果缓存中没有,就从数据库中获取
    			//...如mysql
    			//并且保存到缓存
    		}
    		return value;
    	}
    	/**
    	* CRUD 的删除
    	* @param id id
    	*/
    	@Override
    	public void deleteUser(long id) {
    		//从缓存删除
    		String key = USER_UID_PREFIX + id;
    		cacheOperationService.del(key);
    		//从数据库删除
    		//...如mysql
    		Logger.info("delete User:", id);
    	}
    }
    
  2. 配置“spring-redis.xml”:在业务 Service 类使用 CacheOperationService 缓存管理之前,还需要在配置文件中为其配置依赖:
    <!-- 省略其他的spring-redis.xml配置,具体参见源代码 -->
    <!-- 1、redis 数据源
    	 2、Spring-redis 连接池管理工厂
    	 3、redis template definition
    	 4、将 redisTemplate 封装成通用服务
    -->
    <!-- . . . -->
    
    
    <!--业务 service,依赖缓存 service-->
    <bean id="serviceImplWithTemplate" class="com.crazymakercircle.redis.springJedis.UserServiceImplWithTemplate">
    	<property name="cacheOperationService" ref="cacheOperationService"/>
    </bean>
    
  3. 编写一个用例,测试一下 UserServiceImplWithTemplate,运行之后,可以从 Redis 客户端输入命令来查看缓存的数据。
    至此,缓存机制已经成功生效,数据访问的时间可以从数据库的百毫秒级别缩小到毫秒级别,性能提升了100倍。
    package com.crazymakercircle.redis.springJedis;
    import com.crazymakercircle.im.common.bean.User;
    import com.crazymakercircle.util.Logger;
    import org.junit.Test;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.support.ClassPathXmlApplicationContext;
    public class SpringRedisTester {
    	@Test
    	public void testServiceImplWithTemplate() {
    		ApplicationContext ac = new ClassPathXmlApplicationContext("classpath:spring-redis.xml");
    		UserServiceuserService = (UserService) ac.getBean("serviceImplWithTemplate");
    		long userId = 1L;
    		
    		userService.deleteUser(userId);
    		User userInredis = userService.getUser(userId);
    		Logger.info("delete user", userInredis);
    		
    		User user = new User();
    		user.setUid("1");
    		user.setNickName("foo");
    		userService.saveUser(user);
    		Logger.info("save user:", user);
    		
    		userInredis = userService.getUser(userId);
    		Logger.info("get user", userInredis);
    	}
    	
    	//....省略其他的测试用例
    }
    

使用 RedisCallback 回调完成 CRUD 的实践案例

前面讲到,“RedisConnection 连接类”和“RedisTemplate模板类”都提供了整套 Redis 操作的 API,只不过,它们的层次不同:

  1、RedisConnection 连接类更加底层,它负责二进制层面的 Redis 操作,Key、Value 都是二进制字节数组。

  2、RedisTemplate 模板类,在 RedisConnection 的基础上,使用在 spring-redis.xml 中配置的序列化、反序列化的工具类,完成上层类型(如:String、Object、POJO类等)的 Redis 操作。

如果不需要 RedisTemplate 配置的序列化、反序列化的工具类,或者由于其他的原因,需要直接使用 RedisConnection 去操作 Redis,怎么办呢?

可以使用 RedisCallback 的 doInRedis 回调方法,在 doInRedis 回调方法中,直接使用实参 RedisConnection 连接类实例来完成 Redis 的操作。
  • 当然,完成 RedisCallback 回调业务逻辑后,还需要使用 RedisTemplate 模板实例去执行,调用的是“RedisTemplate.execute(RedisCallback)”方法。


示例:

  1. 通过 RedisCallback 回调方法实现 CRUD 的实例代码如下:UserServiceImplInTemplate 类(实现“UserService”接口,包含“RedisTemplate”成员对象)
    package com.crazymakercircle.redis.springJedis;
    //...
    public class UserServiceImplInTemplate implements UserService {
    	public static final String USER_UID_PREFIX = "user:uid:";
    	private RedisTemplate redisTemplate;
    	private static final long CASHE_LONG = 60 * 4;//4分钟
    	
    	public void setRedisTemplate(RedisTemplate redisTemplate) {
    		this.redisTemplate = redisTemplate;
    	}
    	
    	
    	/**
    	* CRUD 的创建/更新
    	* @param user 用户
    	*/
    	@Override
    	public User saveUser(final User user) {
    		//保存到缓存
    		redisTemplate.execute(new RedisCallback<User>() {
    			@Override
    			public User doInRedis(RedisConnection connection) throws DataAccessException {
    				byte[] key = serializeKey(USER_UID_PREFIX + user.getUid());
    				connection.set(key, serializeValue(user));
    				connection.expire(key, CASHE_LONG);
    				return user;
    			}
    		});
    		
    		//保存到数据库
    		//...如mysql
    		return user;
    	}
    	
    	private byte[] serializeValue(User s) {
    		return redisTemplate.getValueSerializer().serialize(s);
    	}
    	private byte[] serializeKey(String s) {
    		return redisTemplate.getKeySerializer().serialize(s);
    	}
    	private User deSerializeValue(byte[] b) {
    		return (User) redisTemplate.getValueSerializer().deserialize(b);
    	}
    	
    	
    	/**
    	* CRUD 的查询
    	* @param id id
    	* @return 用户
    	*/
    	@Override
    	public User getUser(final long id) {
    		//首先从缓存中获取
    		User value = (User) redisTemplate.execute(new RedisCallback<User>() {
    			@Override
    			public User doInRedis(RedisConnection connection) throws DataAccessException {
    				byte[] key = serializeKey(USER_UID_PREFIX + id);
    				if (connection.exists(key)) {
    					byte[] value = connection.get(key);
    					return deSerializeValue(value);
    				}
    				return null;
    			}
    		});
    		
    		if (null == value) {
    			//如果缓存中没有,从数据库中获取
    			//...如mysql
    			//并且保存到缓存
    		}
    		return value;
    	}
    	
    	
    	/**
    	* CRUD 的删除
    	* @param id id
    	*/
    	@Override
    	public void deleteUser(long id) {
    		//从缓存删除
    		redisTemplate.execute(new RedisCallback<Boolean>() {
    			@Override
    			public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
    				byte[] key = serializeKey(USER_UID_PREFIX + id);
    				if (connection.exists(key)) {
    					connection.del(key);
    				}
    				return true;
    			}
    		});
    		
    		//从数据库删除
    		//...如mysql
    	}
    }
    
  2. 配置“spring-redis.xml”:同样的,在使用 UserServiceImplInTemplate 之前,也需要在配置文件中为其配置好依赖关系:
    <!-- 省略其他的spring-redis.xml配置,具体参见源代码 -->
    <!-- 1、redis 数据源
    	 2、Spring-redis 连接池管理工厂
    	 3、redis template definition
    -->
    <!-- . . . -->
    
    
    <!--业务service,依赖缓存service-->
    <bean id="serviceImplInTemplate" class="com.crazymakercircle.redis.springJedis.UserServiceImplInTemplate">
    	<property name="redisTemplate" ref="redisTemplate"/>
    </bean>