Spring是通过spring-data-redis项目对Redis开发进行支持的。这里仅讨论Spring推荐使用的类库Jedis的使用。
Spring提供了一个RedisConnectionFactory接口,通过它可以生成一个RedisConnection接口对象,而RedisConnection接口对象是对Redis底层接口的封装。例如我们讨论的Jedis驱动,那么Spring就会提供RedisConnection接口的实现类JedisConnection去封装原有的Jedis对象。
在Spring中是通过RedisConnection接口操作Redis的,而RedisConnection则对原生的Jedis进行封装。要获取RedisConnection接口对象,是通过RedisConnectionFactory接口去生成的,所以第一步要配置的便是这个工厂了,而配置这个工厂主要是配置Redis的连接池,对于连接池可以限定其最大连接数、超时时间等属性。
我们在config包下创建名为RedisConfig的配置类,手动装配一个RedisConnectionFactory,具体代码如下:
package com.ccff.springboot.demo.chapter7.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import redis.clients.jedis.JedisPoolConfig;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
@Configuration
public class RedisConfig {
private RedisConnectionFactory connectionFactory = null;
@Bean(name = "redisConnectionFactory")
public RedisConnectionFactory initRedisConnectionFactory(){
if (this.connectionFactory != null){
return this.connectionFactory;
}
JedisPoolConfig poolConfig = new JedisPoolConfig();
//最大空闲数
poolConfig.setMaxIdle(50);
//最大连接数
poolConfig.setMaxTotal(100);
//最大等待时间毫秒数
poolConfig.setMaxWaitMillis(2000);
//创建Jedis连接工厂
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
//获取单机的Redis配置
RedisStandaloneConfiguration configuration = connectionFactory.getStandaloneConfiguration();
configuration.setHostName("127.0.0.1");
configuration.setPort(6379);
configuration.setPassword("123456");
this.connectionFactory = connectionFactory;
return connectionFactory;
}
}
上面的代码通过一个连接池的配置创建了RedisConnectionFactory,通过它就能够创建RedisConnection接口对象。但是我们在使用一条连接时,要先从RedisConnectionFactory工厂获取,然后在使用完成后还要自己关闭它。Spring为了进一步简化开发,提供了RedisTemplate。
应该说RedisTemplate是使用最多的类,所以它是Spring操作Redis的重点内容。
首先它会自动从RedisConnectionFactory工厂中获取连接,然后执行对应的Redis命令,在最后还会关闭Redis连接。这些在RedisTemplate中都被封装了,所以并不需要开发者关注Redis的连接的闭合问题。
修改RedisConfig配置类,手动配置RedisTemplate,具体代码如下:
package com.ccff.springboot.demo.chapter7.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import redis.clients.jedis.JedisPoolConfig;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
@Configuration
public class RedisConfig {
private RedisConnectionFactory connectionFactory = null;
@Bean(name = "redisConnectionFactory")
public RedisConnectionFactory initRedisConnectionFactory(){
if (this.connectionFactory != null){
return this.connectionFactory;
}
JedisPoolConfig poolConfig = new JedisPoolConfig();
//最大空闲数
poolConfig.setMaxIdle(50);
//最大连接数
poolConfig.setMaxTotal(100);
//最大等待时间毫秒数
poolConfig.setMaxWaitMillis(2000);
//创建Jedis连接工厂
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
//获取单机的Redis配置
RedisStandaloneConfiguration configuration = connectionFactory.getStandaloneConfiguration();
configuration.setHostName("127.0.0.1");
configuration.setPort(6379);
configuration.setPassword("123456");
this.connectionFactory = connectionFactory;
return connectionFactory;
}
@Bean(name="redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
}
在手动装配RedisTemplate后,我们就可以使用其操作Redis了。在test文件夹下创建名为Test的测试类,具体代码如下所示:
import com.ccff.springboot.demo.chapter7.config.RedisConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
public class Test {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
RedisTemplate redisTemplate = context.getBean(RedisTemplate.class);
redisTemplate.opsForValue().set("key1","value1");
redisTemplate.opsForHash().put("hash","field","hvalue");
}
}
执行测试方法后,在Redis客户端通过命令“key *key1”进行查看,结果如下:
这里我们需要注意的是:Redis是一种基于字符串存储的NoSQL,而Java是基于对象的语言,对象是无法存储到Redis中的。不过,Java提供了序列化机制,只要类实现了java.io.Serializable接口,就代表类的对象能够进行序列化,通过将类对象进行序列化就能够得到二进制字符串,这样Redis就可以将这些类对象以字符串的形式进行存储。这也就是我们在上面的截图中看到的结果。同时,Java也可以将那些二进制字符串通过反序列化转为对象,通过这个原理,Spring提供了序列化器的机制,并且实现了几个序列化器。
对于序列化器,Spring提供了RedisSerializer接口,它有两个方法。这两个方法,一个是serialize,它能够把那些可以序列化的对象转换为二进制字符串;另一个是deserialize,它能够通过反序列化把二进制字符串转换为Java对象。
在Spring中,最常用的两个序列化器是StringRedisSerializer和JdkSerializationRedisSerializer,其中JdkSerializationRedisSerializer是RedisTemplate默认的序列化器。
RedisTemplate提供了下表所示的几个可以配置的属性
属性 | 描述 | 备注 |
---|---|---|
defaultSerializer | 默认序列化器 | 如果没有设置,则使用JdkSerializationRedisSerializer |
keySerializer | Redis键序列化器 | 如果没有设置,则使用默认序列化器 |
valueSerializer | Redis值序列化器 | 如果没有设置,则使用默认序列化器 |
hashKeySerializer | Redis散列结构field序列化器 | 如果没有设置,则使用默认序列化器 |
hashValueSerializer | Redis散列结构value序列化器 | 如果没有设置,则使用默认序列化器 |
stringSerializer | 字符串序列化器 | RedisTemplate自动赋值为StringSerializer对象 |
为了使我们在Redis客户端显示的结果变为可读的字符串,因此需要修改在RedisConfig配置类中对RedisTemplate的装配。具体修改如下:
@Bean(name="redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate() {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
return redisTemplate;
}
通过上面的代码,主动将Redis的键和散列结构的field和value均采用了字符串序列化器,这样把它们转换出来时就采用字符串了。再次执行测试类后,在Redis的客户端中进行查看,结果如下:
Redis能够支持7种类型的数据结构,这7种类型是:
为此,Spring针对每一种数据结构的操作都提供了对应的操作接口
操作接口 | 功能 | 备注 |
---|---|---|
GeoOperations | 地理位置操作接口 | 使用不多 |
HashOperations | 散列操作接口 | - |
HyperLogLogOperations | 基数操作接口 | 使用不多 |
ListOperations | 列表(链表)操作接口 | - |
SetOperations | 集合操作接口 | - |
ValueOperations | 字符串操作接口 | - |
ZSetOperations | 有序集合操作接口 | - |
而这些操作接口都可以通过RedisTemplate得到,得到的方法也很简单:
//获取地理位置操作接口
redisTemplate.opsForGeo();
//获取散列操作操作接口
redisTemplate.opsForHash();
//获取基数操作操作接口
redisTemplate.opsForHyperLogLog();
//获取列表(链表)操作接口
redisTemplate.opsForList();
//获取集合操作接口
redisTemplate.opsForSet();
//获取字符串操作接口
redisTemplate.opsForValue();
//获取有序集合操作接口
redisTemplate.opsForZSet();
在当前的测试类中,两个操作并不是在同一个Redis的连接下完成的。在执行第一个操作时,redisTemplate会先从工厂中获取一个连接,然后执行对应的Redis命令,再关闭这个连接。在执行第二个操作时,从工厂获取另外一个连接,然后执行对应的Redis命令,再关闭这个连接。通过在控制台输出的日志即可验证
上面所描述的这种情况显然是很浪费资源的,而且更多时候我们希望在同一个连接中执行多个命令。
Spring提供了对应的BoundXXXOperations接口,用于连续操作某个数据类型多次,具体如下:
接口 | 说明 |
---|---|
BoundGeoOperations | 绑定一个地理位置数据类型的键操作,不常用 |
BoundHashOperations | 绑定一个散列数据类型的键操作 |
BoundListOperations | 绑定一个列表(链表)数据类型的键操作 |
BoundSetOperations | 绑定一个集合数据类型的键操作 |
BoundValueOperations | 绑定一个字符串数据类型的键操作 |
BoundZSetperations | 绑定一个有序集合数据类型的键操作 |
同样的,RedisTemplate也对获取它们提供了相应的方法,具体如下:
//获取地理位置绑定键操作接口
redisTemplate.boundGeoOps("geo");
//获取散列绑定键操作接口
redisTemplate.boundHashOps("hash");
//获取列表(链表)绑定键操作接口
redisTemplate.boundListOps("list");
//获取集合绑定键操作接口
redisTemplate.boundSetOps("set");
//获取字符串绑定键操作接口
redisTemplate.boundValueOps("value");
//获取有序列表绑定键操作接口
redisTemplate.boundZSetOps("zset");
在获取其中的操作接口后,我们就恶意对某个键的数据进行多次操作。这样我们再通过SessionCallback接口和RedisCallback接口实现多条命令在同一个连接中完成。
SessionCallback接口和RedisCallback接口的作用是让RedisTemplate进行回调,通过它们可以在同一条连接下执行过个Redis命令。其中SessionCallback提供了良好的封装,对于开发者比较友好,因此在实际开发中应该优先选择使用它;相对而言,RedisCallback接口比较底层,需要处理的内容也比较多,可读性较差,所以在非必要的时候尽量不要使用它。
修改Test测试类的测试方法如下:
import com.ccff.springboot.demo.chapter7.config.RedisConfig;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
public class Test {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(RedisConfig.class);
RedisTemplate redisTemplate = context.getBean(RedisTemplate.class);
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations redisOperations) throws DataAccessException {
redisOperations.opsForValue().set("key1","value1");
redisOperations.opsForHash().put("hash","field","hvalue");
return null;
}
});
}
}
再次执行测试方法,查看输出在控制台的日志信息,发现此事两个Redis命令均在一个连接中执行。
要想在Spring Boot中使用Redis,需要先加入关于Redis的依赖。同样Spring Boot为期提供了starter,然后允许我们在配置文件中进行配置。所以首先我们在pom文件中引入相关依赖,具体如下:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
<exclusions>
<exclusion>
<groupId>io.lettucegroupId>
<artifactId>lettuce-coreartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
dependencies>
这里需要注意的是:在默认情况下,spring-boot-starter-data-redis(版本2.x)会依赖Lettuce的Redis客户端驱动,而在一般的项目中,我们会使用Jedis,所以在代码中使用了exclusions元素将其依赖排除掉,与此同时引入了Jedis的依赖,这样就可以使用Jedis进行编程了。
在引入Redis的相关依赖后,想要使用Redis,则需要在application.properties配置文件中对其进行简单的配置,下面配置了连接池和服务器的属性,用以连接Redis服务器,这样Spring Boot的自动装配机制就会读取这些配置来生成有关Redis的操作对象。这里它会自动生成RedisConnectionFactory、RedisTemplate、StringRedisTemplate等常用的对象。
# =========================== 配置Jedis连接池属性 ===========================
spring.redis.jedis.pool.min-idle=5
spring.redis.jedis.pool.max-active=10
spring.redis.jedis.pool.max-idle=10
spring.redis.jedis.pool.max-wait=2000
# =========================== 配置Jedis连接池属性 ===========================
# =========================== 配置Redis服务器属性 ===========================
spring.redis.port=6579
spring.redis.host=127.0.0.1
spring.redis.timeout=1000
# =========================== 配置Redis服务器属性 ===========================
RedisTemplate会默认使用JdkSerializationSerializer进行序列化键值,这样便能够存储到Redis服务器中。如果这样,Redis服务器存入的便是一个经过序列化的特殊字符串,有时候对于我们跟踪并不是很友好。
如果我们在Redis只是使用字符串,则使用Spring Boot自动为我们生成的StringRedisTemplate即可,但是这样就只能支持字符串了,并不能支持Java对象的存储。
为了克服上述的两种情况产生的问题,我们可以通过设置RedisTemplate的序列化器来处理。
修改Spring Boot的启动类如下:
package com.ccff.springboot.demo.chapter7;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import javax.annotation.PostConstruct;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
@SpringBootApplication
public class Chapter7Application {
//注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate = null;
//定义Spring自定义后初始化方法
@PostConstruct
public void init(){
initRedisTemplate();
}
//设置RedisTemplate的序列化器
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
public static void main(String[] args) {
SpringApplication.run(Chapter7Application.class, args);
}
}
由上面的代码可知,首先自动注入了Spring Boot根据配置文件自动生成的RedisTemplate对象,然后利用Spring Bean生命周期的特性使用注解@PostConstruct自定义后初始化方法。在这个方法里,把RedisTemplate中的键序列化器和散列数据的field都修改为StringRedisSerializer。
由于在RedisTemplate中它会默认地定义一个StringRedisSerializer对象,所以这里并没有自己创建一个新的StringRedisSerializer,而是从RedisTemplate中获取。
在controller包下创建名为RedisController的控制器类,这里用于从RedisTemplate的角度演示常用Redis类型(字符串,散列,列表,集合和有序集合)的操作。初始RedisController的控制器类代码如下:
package com.ccff.springboot.demo.chapter7.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate = null;
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
}
在RedisController的控制器类中创建名为stringAndHash的方法,具体代码如下:
@GetMapping("/stringAndHash")
public Map<String, Object> stringAndHash(){
redisTemplate.opsForValue().set("key1", "value1");
// 注意这里使用了JDK的序列化器,所以Redis保存的时候不是整数,不能运算
redisTemplate.opsForValue().set("int_key", "1");
stringRedisTemplate.opsForValue().set("int", "1");
// 使用运算
stringRedisTemplate.opsForValue().increment("int", 1);
// 获取底层Jedis连接
Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
// 减一操作,这个命令RedisTemplate不支持,所以笔者先获取底层的连接再操作
jedis.decr("int");
Map<String, String> hash = new HashMap<String, String>();
hash.put("field1", "value1");
hash.put("field2", "value2");
// 存入一个散列数据类型
stringRedisTemplate.opsForHash().putAll("hash", hash);
// 新增一个字段
stringRedisTemplate.opsForHash().put("hash", "field3", "value3");
// 绑定散列操作的key,这样可以连续对同一个散列数据类型进行操作
BoundHashOperations hashOps = stringRedisTemplate.boundHashOps("hash");
// 删除两个个字段
hashOps.delete("field1", "field2");
// 新增一个字段
hashOps.put("filed4", "value5");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
列表也是常用的数据类型。在Redis中列表是一种链表结构,因此查询性能不高,但增删节点性能高。在使用链表的过程中需要注意如下两个问题:
在RedisController的控制器类中创建名为testList的方法,具体代码如下:
@GetMapping("/list")
public Map<String, Object> testList() {
// 插入两个列表,注意它们在链表的顺序
// 链表从左到右顺序为v10,v8,v6,v4,v2 —— 对应链表的头插法
stringRedisTemplate.opsForList().leftPushAll("list1", "v2", "v4", "v6", "v8", "v10");
// 链表从左到右顺序为v1,v2,v3,v4,v5,v6 —— 对应链表的尾插法
stringRedisTemplate.opsForList().rightPushAll("list2", "v1", "v2", "v3", "v4", "v5", "v6");
// 绑定list2链表操作
BoundListOperations listOps = stringRedisTemplate.boundListOps("list2");
// 从右边弹出一个成员
Object result1 = listOps.rightPop();
// 获取定位元素,Redis从0开始计算,这里值为v2
Object result2 = listOps.index(1);
// 从左边插入链表
listOps.leftPush("v0");
// 求链表长度
Long size = listOps.size();
// 求链表下标区间成员,整个链表下标范围为0到size-1,这里不取最后一个元素
List elements = listOps.range(0, size - 2);
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
对于集合,在Redis中是不允许集合中的元素有重复的,它在数据结构上是一个散列表的结构,所以对于它而言是无序的,对于两个或者两个以上的集合,Redis还提供了交集、并集和差集的运算。
在RedisController的控制器类中创建名为testSet的方法,具体代码如下:
@GetMapping("/set")
public Map<String, Object> testSet() {
// 请注意:这里v1重复2次,由于集合不允许重复,所以只是插入5个成员到集合中
stringRedisTemplate.opsForSet().add("set1", "v1", "v1", "v2", "v3", "v4", "v5");
stringRedisTemplate.opsForSet().add("set2", "v2", "v4", "v6", "v8");
// 绑定set1集合操作
BoundSetOperations setOps = stringRedisTemplate.boundSetOps("set1");
// 增加两个元素
setOps.add("v6", "v7");
// 删除两个元素
setOps.remove("v1", "v7");
// 返回所有元素
Set set1 = setOps.members();
// 求成员数
Long size = setOps.size();
// 求交集
Set inter = setOps.intersect("set2");
// 求交集,并且用新集合inter保存
setOps.intersectAndStore("set2", "inter");
// 求差集
Set diff = setOps.diff("set2");
// 求差集,并且用新集合diff保存
setOps.diffAndStore("set2", "diff");
// 求并集
Set union = setOps.union("set2");
// 求并集,并且用新集合union保存
setOps.unionAndStore("set2", "union");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
有序集合与集合的差异并不大,它也是一种散列表存储的方式,同时它的有序性只是靠它在数据结构中增加了一个属性 —— score(分数)得以支持。为了支持这个变化,Spring提供了TypedTuple接口方法,并且Spring还提供了其默认的实现类DefaultTypedTuple。
在TypedTuple接口的设计中,value是保存有序集合的值,score则是保存分数,Redis是使用分数来完成集合的排序的。
在默认情况下,有序集合是从小到大地排序的,按下标、分数和值进行排序获取有序集合的元素,或者连同分数一起返回,有时还可以进行从大到小的排序,只是在使用排序时,我们可以使用SPring为我们创建的Range类,它可以定义值的范围,还有大于、等于、大于等于、小于等于等范围定义,方便筛选对应元素。
在RedisController的控制器类中创建名为testSet的方法,具体代码如下:
@GetMapping("/zset")
public Map<String, Object> testZset() {
Set<TypedTuple<String>> typedTupleSet = new HashSet<>();
for (int i = 1; i <= 9; i++) {
// 分数
double score = i * 0.1;
// 创建一个TypedTuple对象,存入值和分数
TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value" + i, score);
typedTupleSet.add(typedTuple);
}
// 往有序集合插入元素
stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
// 绑定zset1有序集合操作
BoundZSetOperations<String, String> zsetOps = stringRedisTemplate.boundZSetOps("zset1");
// 增加一个元素
zsetOps.add("value10", 0.26);
Set<String> setRange = zsetOps.range(1, 6);
// 按分数排序获取有序集合
Set<String> setScore = zsetOps.rangeByScore(0.2, 0.6);
// 定义值范围
Range range = new Range();
range.gt("value3");// 大于value3
// range.gte("value3");// 大于等于value3
// range.lt("value8");// 小于value8
range.lte("value8");// 小于等于value8
// 按值排序,请注意这个排序是按字符串排序
Set<String> setLex = zsetOps.rangeByLex(range);
// 删除元素
zsetOps.remove("value9", "value2");
// 求分数
Double score = zsetOps.score("value8");
// 在下标区间下,按分数排序,同时返回value和score
Set<TypedTuple<String>> rangeSet = zsetOps.rangeWithScores(1, 6);
// 在分数区间下,按分数排序,同时返回value和score
Set<TypedTuple<String>> scoreSet = zsetOps.rangeByScoreWithScores(1, 6);
// 按从大到小排序
Set<String> reverseSet = zsetOps.reverseRange(2, 8);
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
Redis是支持一定事务能力的NoSQL。在Redis中使用事务,通常的命令组合是watch……multi……exec。也就是说要在一个Redis连接中执行多个命令,这时我们可以考虑使用SessionCallback接口来达到目的。
watch命令是可以监控Redis的一些键。
multi命令是开始事务。开始事务后,该客户端的命令不会马上被执行,而是存放在一个队列中,这点是需要注意的地方。也就是说在这时我们执行一些返回数据的命令,Redis也是不会马上执行的,而是把命令放入一个队列中,所以此时调用Redis的命令,结果都是返回null的,这时初学者容易犯的错误。
exec命令的意义在于执行事务。只是它在队列命令执行前会判断被watch监控的Redis键的数据是否发生过变化(即使赋予之前相同的值也会被认为是变化过的),如果它认为发生了变化,那么Redis就会取消事务,否则就会执行事务。
Redis在执行事务时,要么全部执行,要么全部不执行,而且不会被其他客户端打断,这样就保证了Redis事务下的数据一致性。
为了测试Redis事务,在RedisController类中添加testMulti方法用于测试事务,具体代码如下:
@GetMapping("/testMulti")
public Map<String, Object> testMulti() {
redisTemplate.opsForValue().set("key1", "value1");
List list = (List) redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
// 设置要监控key1
operations.watch("key1");
// 开启事务,在exec命令执行前,全部都只是进入队列
operations.multi();
operations.opsForValue().set("key2", "value2");
operations.opsForValue().set("key1", "update_value1");// ①
// 获取值将为null,因为redis只是把命令放入队列,
Object value2 = operations.opsForValue().get("key2");
System.out.println("命令在队列,所以value为null【" + value2 + "】");
operations.opsForValue().increment("key1",1); // ②
operations.opsForValue().set("key3", "value3");
Object value3 = operations.opsForValue().get("key3");
System.out.println("命令在队列,所以value为null【" + value3 + "】");
// 执行exec命令,将先判别key1是否在监控后被修改过,如果是不执行事务,否则执行事务
return operations.exec();// ③
}
});
System.out.println(list);
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
接下来根据代码中的①、②、③处进行测试。
测试一:将代码行①,代码行②注释掉,执行启动类,在浏览器内输入对应的URL后,查看结果发现key2和key3对应的值已经存入了Redis中。且由于multi后的命令不是直接执行而是进入队列的,因此在事务中获取key2和key3的值为null。在控制台和Redis客户端查看结果分别如下:
测试二:将代码行①,代码行②注释掉,然后在代码③行添加断点,进入debug模式后在浏览器输入对应URL后,请求将抵达断点处。此时在Redis客户端通过命令修改key1的值为“update_value1”后,继续执行直至完成。此时发现key2和key3的值均为空(nil)。这就说明了当监控的键key1发生变化后,事务将不再执行。在控制台和Redis客户端查看结果分别如下:
测试三:将代码行①注释去掉,代码行②仍然注释,去掉代码③行的断点。执行启动类,在浏览器内输入对应的URL后,发现事务正常执行。在控制台和Redis客户端查看结果分别如下:
这里大家是否会有这样的疑问:为什么在代码行①处明明对监控的键key1的值进行了修改,为什么事务还能成功呢? 这里的确有一个坑,但是仔细想想其实事情并不是这样的,由于通过multi开启事务后,所有的命令都是进入队列的,而不是直接执行的。也就是说在代码行①处对key1的值的修改并没有执行,那么在事务执行时对key1的值进行判断是否发生变化时自然为否,则执行事务。而代码行①处的代码仅仅是作为事务中一条普通的命令执行了,只不过该命令是修改的key1的值而已。
测试四:将代码行①注释保留,代码行②注释取消,去掉代码③行的断点。执行启动类,在浏览器内输入对应的URL后,由于代码行②处的命令是对字符串加一,显然这是不能运算的,因此在控制台会报异常。但当我们在Redis客户端查看key2和key3的值时,发现它们此时已经具有了值。在控制台和Redis客户端查看结果分别如下:
这里需要注意的是: 这就是Redis事务和数据库事务的不一样的地方。对于Redis事务,是先让命令进入队列,所以一开始它并没有检测这个加一命令是否能够执行成功,只有在exec命令执行的时候,才能发现错误,对于出错的命令Redis只是报出错误,而错误后面的命令依旧被执行, 所以key2和key3的值都存在数据,这就是Redis事务的特点,也是使用Redis事务需要特别注意的地方。为了克服这个问题,一般我们要在执行Redis事务前,严格地检查数据,以免发生这样的情况。
在默认情况下,Redis客户端是一条一条命令发送给Redis服务器的,这样显然性能不高。因此Redis也可以使用流水线(pipeline)技术大幅度地在需要执行很多命令时提升Redis的性能。
为了测试流水线技术,在RedisController中添加方法testPipeline,具体代码如下:
@GetMapping("/testPipeline")
public Map<String, Object> testPipeline() {
Long start = System.currentTimeMillis();
List list = (List) redisTemplate.executePipelined((RedisOperations operations) -> {
for (int i = 1; i <= 100000; i++) {
operations.opsForValue().set("pipeline_" + i, "value_" + i);
String value = (String) operations.opsForValue().get("pipeline_" + i);
if (i == 100000) {
System.out.println("命令只是进入队列,所以值为空【" + value + "】");
}
}
return null;
});
Long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "毫秒。");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
上述代码沿用了SessionCallback接口执行写入和读出各10万次Redis命令,并打印出耗时。
这里需要注意以下两点:
首先是Redis提供一个渠道,让消息能够发送到这个渠道上,而多个系统可以监听这个渠道,如短信、微信和邮件系统都可以监听这个渠道,当一条消息发送到渠道,渠道就会通知它的监听者,这样短信,微信和邮件系统就能够得到这个渠道给它们的消息了,这些监听者会根据自己的需要去处理这个消息。
为了接收Redis渠道发送过来的消息,需要在listener包中定义一个消息监听器,具体代码如下:
package com.ccff.springboot.demo.chapter7.listener;
import com.ccff.springboot.demo.chapter7.controller.RedisController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
* Created by wangzhefeng01 on 2019/8/12.
*/
@Component
public class RedisMessageListener implements MessageListener {
@Override
public void onMessage(Message message, @Nullable byte[] pattern) {
//消息体
String messageBody = new String(message.getBody());
//渠道名称
String topic = new String(pattern);
System.out.println("消息体: "+messageBody);
System.out.println("渠道名称: "+topic);
}
}
上述代码中的onMessage方法是得到消息后的处理方法,其中message参数代表Redis发送过来的消息,pattern是渠道名称。这里因为标注了注解@Component,所以在Spring Boot扫描后,会把它自动装配到IoC容器中。
接着我们需要在Spring Boot启动类中配置其他信息,让系统能够监控Redis的消息,具体代码如下:
package com.ccff.springboot.demo.chapter7;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import javax.annotation.PostConstruct;
/**
* Created by wangzhefeng01 on 2019/8/9.
*/
@SpringBootApplication
public class Chapter7Application {
//注入RedisTemplate
@Autowired
private RedisTemplate redisTemplate = null;
//注入Redis连接工厂
@Autowired
private RedisConnectionFactory connectionFactory = null;
//注入Redis消息监听器
@Autowired
private MessageListener redisMessageListener = null;
//任务池
private ThreadPoolTaskScheduler taskScheduler = null;
//定义Spring自定义后初始化方法
@PostConstruct
public void init(){
initRedisTemplate();
}
/**
* 设置RedisTemplate的序列化器
*/
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
/**
* 创建任务池,运行线程等待处理Redis的消息
* @return
*/
@Bean
public ThreadPoolTaskScheduler initTaskScheduler(){
if (taskScheduler != null){
return taskScheduler;
}
taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(20);
return taskScheduler;
}
/**
* 定义Redis的监听容器
* @return
*/
@Bean
public RedisMessageListenerContainer initRedisContainer(){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
//Redis连接工厂
container.setConnectionFactory(connectionFactory);
//设置运行任务池
container.setTaskExecutor(initTaskScheduler());
//定义监听渠道,名称为topic1
Topic topic = new ChannelTopic("topic1");
//使用监听器监听Redis的消息
container.addMessageListener(redisMessageListener,topic);
return container;
}
public static void main(String[] args) {
SpringApplication.run(Chapter7Application.class, args);
}
}
在上面的代码中,RedisTemplate和RedisConnectionFactory对象都是Spring Boot自动创建的,所以这里只是把它们注入进来,只需要注解@Autowired即可。然后定义了一个任务池,并设置了任务池的大小为20,这样它将可以运行线程,等待Redis消息的传入。接着再定义了一个Redis消息监听容器RedisMessageListenerContainer,并往容器设置了Redis连接工厂和指定运行消息的线程池,定义了接收“topic1”渠道的消息,这样系统就可以监听Redis关于“topic1”渠道的消息了。
启动Spring Boot项目后,在Redis的客户端输入命令:
publish topic1 message1
在控制台可看到如下信息:
在Spring在,我们也可以使用RedisTemplate来发送消息:
//channel:代表渠道
//message:代表消息
redisTemplate.convertAndSend(channel,message);
为了增强Redis的计算能力,Redis在2.6版本后提供了Lua脚本的支持,而且执行Lua脚本在Redis中还具备原子性,所以在需要保证数据一致性的高并发环境中,我们也可以使用Redis的Lua语言来保证数据的一致性,且Lua脚本具备更加强大的运算功能,在高并发需要保证数据一致性时,Lua脚本方案比使用Redis自身提供的事务要更好一些。
在Redis中运行Lua的方法有如下两种:
这里需要解释的是为什么会存在通过32位编码执行的方法。如果Lua脚本很长,那么就需要通过网络传递脚本给Redis去执行了,而现实的情况是网络的传递速度往往跟不上Redis的执行速度,所以网络就会变成Redis的执行瓶颈。如果只传递32位编码和参数,那么需要传递的消息就少了许多,这样就可以极大地减少网络传输的内容,从而提高系统的性能。
为了支持Redis的Lua脚本,Spring提供了RedisScript接口,与此同时也有一个DefaultRedisScript实现类。RedisScript接口具有如下三个主要方法:
在RedisController中添加一个执行十分简单的Lua脚本的测试方法testLua,具体代码如下:
@GetMapping("/lua")
public Map<String, Object> testLua() {
DefaultRedisScript<String> rs = new DefaultRedisScript<String>();
// 设置脚本
rs.setScriptText("return 'Hello Redis'");
// 定义返回类型,注意如果没有这个定义Spring不会返回结果
rs.setResultType(String.class);
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
// 执行Lua脚本
String str = (String) redisTemplate.execute(rs, stringSerializer, stringSerializer, null);
Map<String, Object> map = new HashMap<String, Object>();
map.put("str", str);
return map;
}
在上面的代码中,首先Lua只是定义了一个简单的字符串,然后就返回了,而返回的类型则定义为字符串。这里必须定义返回类型,否则Spring不会把脚本执行的结果返回。 接着获取了由RedisTemplate自动创建的字符串序列化器,而后使用RedisTemplate的execute方法执行了脚本。在RedisTemplate中,execute方法执行脚本的方法有如下两种:
public <T> T execute(RedisScript<T> script, List<K> keys, Object... args) { /* compiled code */ }
public <T> T execute(RedisScript<T> script, RedisSerializer<?> argsSerializer, RedisSerializer<T> resultSerializer, List<K> keys, Object... args) { /* compiled code */ }
在这两个方法中,从参数的名称可以知道:
上述两个方法最大区别就是一个存在序列化器的参数,另一个不存在。对于不存在序列化参数的方法,Spring将采用RedisTemplate提供的valueSerializer序列化器对传递的键和参数进行序列化。
上面的Lua脚本十分简单,下面考虑存在参数的情况,例如下面的Lua脚本,用来判断两个字符串是否相同,具体如下:
redis.call('set', KEYS[1], ARGV[1])
redis.call('set', KEYS[2], ARGV[2])
local str1 = redis.call('get', KEYS[1])
local str2 = redis.call('get', KEYS[2])
if str1 == str2 then
eturn 1
end
return 0
这里的脚本中使用了两个键来保存两个参数,然后对这两个参数进行比较,如果相等则返回1,否则返回0。注意脚本中的KEYS[1]和KEYS[2]的写法,它们代表客户端传递的第一个键和第二个键,而ARGV[1]和ARGV[2]则表示客户端传递的第一个和第二个参数。
在RedisController中添加执行带有参数的Lua脚本的测试方法testLua2,具体代码如下:
@GetMapping("/lua2")
public Map<String, Object> testLua2(String key1, String key2, String value1, String value2) {
// 定义Lua脚本
String lua = " redis.call('set', KEYS[1], ARGV[1]) \n"
+ " redis.call('set', KEYS[2], ARGV[2]) \n"
+ " local str1 = redis.call('get', KEYS[1]) \n"
+ " local str2 = redis.call('get', KEYS[2]) \n"
+ " if str1 == str2 then \n" + "return 1 \n"
+ " end \n"
+ " return 0 \n";
System.out.println(lua);
// 结果返回为Long
DefaultRedisScript<Long> rs = new DefaultRedisScript<Long>();
rs.setScriptText(lua);
rs.setResultType(Long.class);
// 采用字符串序列化器
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
// 定义key参数
List<String> keyList = new ArrayList<>();
keyList.add(key1);
keyList.add(key2);
// 传递两个参数值,其中第一个序列化器是key的序列化器,第二个序列化器是参数的序列化器
Long result = (Long) redisTemplate.execute(rs, stringSerializer, stringSerializer, keyList, value1, value2);
Map<String, Object> map = new HashMap<String, Object>();
map.put("result", result);
return map;
}
在上述代码中使用了keyList保存了各个键,然后通过Redis的execute方法传递,参数则可以使用可变化的方式传递,且设置了给键和参数的序列化器都是字符串序列化器,这样便能够运行这段脚本了。脚本返回一个数字,这里值得注意的是:因为Java会把整数当做长整型(Long),所以这里的返回值设置为Long。