Redis是一种运行在内存中的数据库,支持7种数据类型的存储,分别是字符串、散列、列表(链表)、集合、有序集合、基数和地理位置。Redis是一个开源、使用ANSI C语言编写、遵守BSD协议、支持网络、可基于内存亦可持久化的日志型、键值数据库,并提供多种语言的API。Redis是基于内存的,所以读写速度很快,大约是关系数据库几倍到几十倍的速度。如果将常用的数据存储在Redis中,用来代替关系数据库的查询访问,网站的性能将可以大幅提高。
Redis还提供了简单的事务机制,通过事务机制可以有效的保证在高并发的场景下数据的一致性。Redis自身数据类型比较少,命令功能也比较有限,运算能力一直不强,所以在Redis2.6版本之后增加了一个Lua语言的支持,这样Redis的计算能力就大大提高了,而且在Redis中Lua语言的执行是原子性的,也就是在Redis执行Lua时,不会被其他命令所打断,这样就能够保证在高并发场景下的一致性。
要使用Redis,需要加入关于Redis的依赖,通过application.properties进行配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依赖Redis的异步客户端-->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入Redis的客户端驱动-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring-data-redis项目简介
- spring-data-redis项目的设计
在java中与redis的连接驱动有多种,目前比较广泛使用的是jedis,其他还有lettuce、jredis和srp。lettuce目前使用得比较少,而jredis和srp则已经不再被推荐使用,所以这里只讨论jedis的使用。
spring提供了一个RedisConnetionFactory接口,通过它可以生成一个RedisConnection接口对象,而RedisConnection接口对象是对Redis底层接口的封装。使用jedis驱动,spring就会提供RedisConnection接口的实现类JedisConnection去封装原有的jedis对象。
在spring中是通过RedisConnection接口操作Redis的,而RedisConnection则对原生的jedis进行封装。要获取RedisConnection接口对象,是通过RedisConnectionFactory接口去生成的,所以要配置RedisConnectionFactory工厂,而配置这个工厂主要是配置Redis的连接池。如下所示。
@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(30);
//最大连接数
poolConfig.setMaxTotal(50);
//最大等待毫秒数
poolConfig.setMaxWaitMillis(2000);
//创建Jedis连接工厂
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(poolConfig);
//获取单机的Redis设置
RedisStandaloneConfiguration standaloneConfiguration = connectionFactory.getStandaloneConfiguration();
standaloneConfiguration.setHostName("192.168.1.103");
standaloneConfiguration.setPort(6379);
standaloneConfiguration.setPassword(RedisPassword.of(redis服务器密码"));
this.connectionFactory = connectionFactory;
return connectionFactory;
}
}
通过一个连接池创建了RedisConnectionFactory,通过它就能创建RedisConnection接口对象。在使用一条连接时,要先从RedisConnectionFactory工厂获取,然后在使用完成时自己关闭它。spring为了进一步简化开发,提供了RedisTemplate。
- RedisTemplate
RedisTemplate 自动从RedisConnectionFactory工厂中获取连接,然后执行对应的Redis命令,在最后还会关闭Redis的连接。在上述代码中加入如下代码:
@Bean(name = "redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate(){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
测试代码
package com.springboot.chapter07.main;
public class Chapter07Main {
public static void main(String[] args) {
// TODO Auto-generated method stub
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
@SuppressWarnings("unchecked")
RedisTemplate<String, String> redisTemplate = ctx.getBean(RedisTemplate.class);
redisTemplate.opsForValue().set("key2", "value2");
redisTemplate.opsForHash().put("hash", "field","hvalue");
}
}
这里使用了java配置文件RedisConfig来创建SpringIOC容器,然后从中获取RedisTemplate对象,接着设置一个键为“key2”值为“value2”的键值对,。运行这段代码后,可以在Redis客户端输入命令“keys *key2”,如下所示
可以看到,Redis存入的并不是“key2”字样的字符串。首先要弄清楚的是Redis是一种基于字符串存储的NoSQL,而java是基于对象的语言,对象是无法存储到Redis中的,不过java提供了序列化机制,只要实现了java.io.Serializable接口,就代表类的对象能过进行序列化,通过将类对象进行序列化就能够得到二进制字符串,这样Redis就可以将这些类对象以字符串的进行存储。java也可以将那些二进制字符串通过反序列化转为对象,通过这个原理,spring提供了序列化器的机制,并且实现了几个序列化器。
对于序列化器,Spring提供了RedisSerializer接口,他有两个方法:一个是serialize,能够吧那些可以序列化的对象转换为二进制字符串;另一个是deserialize,能够通过反序列化把二进制字符串转换为java对象。这里主要讨论StringRedisSerializer和JdkSerializationRedisSerializer,其中JdkSerializationRedisSerializer是RedisTemplate默认的序列化器,上述代码中的“key2”这个字符串就是被他序列化变为一个比较奇怪的字符串的,其原理如下图所示。
在initRedisTemplate()方法中,他会默认使用JdkSerializationRedisSerializer对对象进行序列化和反序列化,这就是Redis服务器得到那些复杂字符串的原因。为了解决这个问题,希望RedisTemplate可以将Redis的键以普通字符串保存。所以将代码改成如下:
@Bean(name = "redisTemplate")
public RedisTemplate<Object, Object> initRedisTemplate(){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<Object, Object>();
//RedisTemplate会自动初始化StringRedisSerializer,所以这里可以直接获取
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
//设置字符串序列化器,String就会把Redis的key当做字符串处理
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
redisTemplate.setHashValueSerializer(stringSerializer);
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
这里,通过主动将Redis的键和散列结构的filed和value均采用了字符串序列化器,这样把他们转换出来时就会采用字符串了。运行代码后,再次查询Redis的数据,结果如下:
从图中可以看到,redis的键已经从复杂的编码变成简单的字符串了,而hash数据类型全部采用了字符串的形式,这时因为设置了使用StringRedisSerializer序列化器操作他们。
注意下面代码:
redisTemplate.opsForValue().set("key2", "value2");
redisTemplate.opsForHash().put("hash", "field","hvalue");
这两个操作并不是在同一个Redis的连接下完成的
代码执行的时候Console窗口打印了两次opening和closing,说明RedisConnection连接和关闭了两次,这样显然存在资源的浪费,我们希望的是在同一条连接中就执行两个命令。
为了解决这个问题,spring提供了RedisCallback和SessionCallback两个接口。
- Spring对Redis数据类型操作的封装
开头有说道,Redis有7种数据类型,分别是字符串、散列、列表(链表)、集合、有序集合、基数和地理位置。为此spring针对每一种数据结构的操作都提供了对应的操作接口
GeoOperations //地理位置操作接口
HashOperations //散列操作接口
HyperLogLogOperations //基数操作接口
ListOperations //列表操作接口
SetOperations //集合操作接口
ValueOperactions //字符串操作接口
ZSetOperactions //有序集合操作接口
他们都可以通过RedisTemplate得到:
redisTemplate.opsForGeo();//获取地理位置操作接口
redisTemplate.opsForHash();//获取散列操作接口
redisTemplate.opsForHyperLogLog();//获取基数操作接口
redisTemplate.opsForList();//获取列表操作接口
redisTemplate.opsForSet();//获取集合操作接口
redisTemplate.opsForValue();//获取字符串操作接口
redisTemplate.opsForZSet();//获取有序集合操作接口
这样就可以通过各类的操作接口来操作不同的数据类型了。有时候可能需要对某一个键值对左连续的操作,例如,有时需要连续操作一个散列数据类型或者列表多次,这时spring也提供支持,它提供了对应的BoundXXXOperations接口,如下所示:
BoundGeoOperations //绑定一个地理位置数据类型的键操作
BoundHashOperations //绑定一个散列数据类型的键操作
BoundListOperations //绑定一个列表数据类型的键操作
BoundSetOperations //绑定一个集合数据类型的键操作
BoundValueOperations //绑定一个字符串数据类型的键操作
BoundZSetOperations //绑定一个有序集合数据类型的键操作
同样的,RedisTemplate也对获取他们提供了对应的方法
redisTemplate.boundGeoOps("geo");//获取地理位置绑定键操作接口
redisTemplate.boundHashOps("hash");//获取散列绑定键操作接口
redisTemplate.boundListOps("list");//获取列表绑定键操作接口
redisTemplate.boundSetOps("set");//获取集合绑定键操作接口
redisTemplate.boundValueOps("value");//获取字符串绑定键操作接口
redisTemplate.boundZSetOps("zset");//获取有序集合绑定键操作接口
获取其中的接口后,就可以对某个键的数据进行多次操作,这样就知道如何有效的通过spring操作redis的各种数据了
- SessionCallback和RedsiCallback接口
SessionCallback和RedsiCallback接口的作用是让RedisTemplate进行回调,通过他们可以在同一条连接下执行多个redis命令。其中SessionCallback提供良好的封装,对于开发者比较友好,因此在实际的开发中应该优先选择;相对而言,RedisCallback接口比较底层,需要处理的内容比较多,可读性较差,所以在不必要的情况下使用他。
//需要底层的转换规则,如果不考虑改写底层,尽量不要使用
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void useRedisCallback(RedisTemplate redisTemplate) {
redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
// TODO Auto-generated method stub
connection.set("key1".getBytes(), "value1".getBytes());
connection.hSet("hash".getBytes(), "field".getBytes(), "hvalue".getBytes());
return null;
}
});
}
//高级接口,比较友好,一般情况下,优先使用
@SuppressWarnings({ "rawtypes", "unchecked" })
public static void useSessionCallback(RedisTemplate redisTemplate) {
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
// TODO Auto-generated method stub
operations.opsForValue().set("key1", "value1");
operations.opsForHash().put("hash", "field", "hvalue");
return null;
}
});
}
使用Lambda表达式改写上述代码:
//使用Lambda表达式
@SuppressWarnings({ "unchecked", "rawtypes" })
public static void useSessionCallback(RedisTemplate redisTemplate) {
redisTemplate.execute((RedisOperations operations)->{
operations.opsForValue().set("key1", "fvbdfbvaiu");
operations.opsForHash().put("hash", "field", "ksdvakvbh");
return null;
});
}
SessionCallback和RedsiCallback接口都能够使得RedisTemplate使用同一条连接进行回调,从而可以在同一条Redis连接下执行多个方法,避免RedisTemplate多次获取不同的连接。
在Springboot中配置和使用Redis
- 在springboot中配置redis
在application.properties文件中加入如下代码:
#配置连接池属性
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
#配置Redis服务器属性
spring.redis.port=6379
spring.redis.host=192.168.1.103
spring.redis.password=9602242138xq
#Redis连接超时时间,单位毫秒
spring.redis.timeout=1000
通过配置连接池和服务器属性,springboot的自动装配机制就会读取这些配置来生成有关Redis的操作对象,这里它会自动生成RedisConnectionFactory、RedisTemplate、StringRedisTemplate等常用的Redis对象。因为RedisTemplate会默认使用JdkSerializationRedisSerializer进行序列化键值,这样便能够存储到Redis服务器中。如果这样,Redis服务器存入的便是一个经过序列化后的特殊字符串,有时候对于我们的跟踪并不是很友好。如果我们在redis只是使用字符串,那么使用其自动生成的StringRedisTemplate即可,但是这样就只能支持字符串了,并不能支持java对象的存储。
为了克服这个问题,可以通过设置RedisTemplate的序列化来处理。
@Autowired
private RedisTemplate redisTemplate = null;
//定义自定义后初始化方法
@PostConstruct
public void init() {
initRedisTemplate();
}
//设置RedisTemplate的序列化器
@SuppressWarnings({ "rawtypes", "unchecked" })
private void initRedisTemplate() {
// TODO Auto-generated method stub
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
- 操作Redis数据类型
操作Redis字符串和散列数据类型
@Controller
@RequestMapping("/redis")
public class RedisController {
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate = null;
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
//操作Redis字符串和散列数据类型
@SuppressWarnings("unchecked")
@RequestMapping("/testStringAndHash")
@ResponseBody
public Map<String, Object> testStringAndHash(){
redisTemplate.opsForValue().set("key1", "abc");
//这里用的是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();
//减1操作,这个命令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<String,Object,Object> hashOps = stringRedisTemplate.boundHashOps("hash");
//删除两个字段
hashOps.delete("field1","field2");
//新增一个字段
hashOps.put("field4", "value4");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
}
使用spring操作列表
列表元素中下标以0开始
//操作链表
@RequestMapping("/list")
@ResponseBody
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<String,String> boundListOps = stringRedisTemplate.boundListOps("list2");
//从右边弹出一个成员
String result1 = boundListOps.rightPop();
System.out.println("rightpop()="+result1);
//获取定位元素,Redis从0开始计算,这里值为v2
String result2 = boundListOps.index(0);
System.out.println("index(0)="+result2);
//从左边插入列表
boundListOps.leftPush("v0");
//列表的长度
Long size = boundListOps.size();
System.out.println("列表的长度:"+size);
//求链表下区间成员,整个列表下表范围为0到size-1,这里不取最后一个元素
List<String> elements = boundListOps.range(0, size-2);
for (String string : elements) {
System.out.print(string+",");
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
使用spring操作集合
//操作集合
@RequestMapping("/set")
@ResponseBody
public Map<String, Object> testSet(){
//这里插入了两个s1,因为set集合不许重复,所以只有一个s1插入
// stringRedisTemplate.opsForSet().add("set1", "s1","s1","s2","s3","s4","s5");
// stringRedisTemplate.opsForSet().add("set2", "s2","s4","s6","s8");
//对set1进行绑定操作
BoundSetOperations<String,String> boundSetOps = stringRedisTemplate.boundSetOps("set1");
//增加两个元素
// boundSetOps.add("s6","s7");
//删除两个元素
// boundSetOps.remove("s1","s7");
//返回所有元素
Set<String> members = boundSetOps.members();
for (String string : members) {
System.out.print(string+",");
}
//求成员数
Long size = boundSetOps.size();
System.out.println("成员数:"+size);
//求交集
Set<String> intersect = boundSetOps.intersect("set2");
System.out.println("交集:");
for (String string : intersect) {
System.out.print(string+",");
}
//求交集,并用新集合inter保存
// boundSetOps.intersectAndStore("set2", "inter");
//求差集
Set<String> diff = boundSetOps.diff("set2");
System.out.println("差集:");
for (String string : diff) {
System.out.print(string+",");
}
//求差集,并用diff保存
// boundSetOps.diffAndStore("set2", "diff");
//求并集
Set<String> union = boundSetOps.union("set2");
System.out.println("并集:");
for (String string : union) {
System.out.print(string+",");
}
//求并集,并用union保存
// boundSetOps.unionAndStore("set2", "union");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
操作Redis有序集合
//操作有序集合
@RequestMapping("/zset")
@ResponseBody
public Map<String, Object> testZset(){
Set<TypedTuple<String>> typedTupleSet = new HashSet<TypedTuple<String>>();
for (int i = 0; i <=9; i++) {
//分数
double score = i*0.1;
//创建一个typeTuple对象,存入值和分数
TypedTuple<String> typedTuple = new DefaultTypedTuple<String>("value"+i, score);
typedTupleSet.add(typedTuple);
}
//往有序集合插入元素
// stringRedisTemplate.opsForZSet().add("zset1", typedTupleSet);
//绑定zset1有序集合操作
BoundZSetOperations<String,String> boundZSetOps = stringRedisTemplate.boundZSetOps("zset1");
//增加一个元素
// boundZSetOps.add("value10", 0.26);
//返回下标是1到6之间的元素
Set<String> range = boundZSetOps.range(1, 6);
for (String string : range) {
System.out.print(string+",");//value1,value2,value3,value4,value5,value6
}
//按分数排序获取有序集合
Set<String> rangeByScore = boundZSetOps.rangeByScore(0.2, 0.6);
System.out.println("按分数排序获取有序集合:");
for (String string : rangeByScore) {
System.out.println(string+",");//value2,value3,value4,value5
}
//删除元素
// Long remove = boundZSetOps.remove("value9","value10");
// System.out.println(remove);//2
//求分数
Double score = boundZSetOps.score("value8");
System.out.println("score="+score);//0.8
//在下标区间下,按分数排序,同时返回value和score
Set<TypedTuple<String>> rangeWithScores = boundZSetOps.rangeWithScores(1, 6);
System.out.println("在下标区间下,按分数排序,同时返回value和score:");
for (TypedTuple<String> string : rangeWithScores) {
System.out.println(string.getScore()+",");
}
//在分数区间下,按分数排序,同时返回value和score
Set<TypedTuple<String>> rangeByScoreWithScores = boundZSetOps.rangeByScoreWithScores(0.2, 0.6);
System.out.println("在分数区间下,按分数排序,同时返回value和score:");
for (TypedTuple<String> string : rangeByScoreWithScores) {
System.out.println(string.getScore()+",");
}
//按从大到小排序
Set<String> reverseRange = boundZSetOps.reverseRange(2, 8);
System.out.println("按从大到小排序:");
for (String string : reverseRange) {
System.out.println(string+",");
}
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
Redis的一些特殊用法
Redis除了操作那些数据类型的功能外,还能支持事务、流水线、发布订阅和Lua脚本等功能。在高并发的场景中,往往还需要保证数据的一致性,这时需要考虑使用Redis事务或者利用Redis执行Lua的原子性来达到数据一致性的目的。在需要大批量执行Redis命令的时候,可以使用流水线来执行命令,这样就可以极大的提高Redis执行的速度
- 使用Redis事务
在Redis中使用事务,通常用的命令组合是watch…multi…exec,也就是要在一个Redis连接中执行多个命令,这时可以使用SessionCallback接口来达到这个目的。其中,watch命令是可以监控Redis的一些键;multi命令是开启事务,开始事务后,该客户端的命令不会马上被执行,而是存放在一个队列里,所以此时调用Redis的命令,结果都会返回null;exec命令执行事务,只是他会在队列命令执行前会判断被watch监控的Redis的键的数据是否发生过变化(即使赋予之前相同的值也会被认为是变化过),如果他认为发生了变化,那么Redis就会取消事务,否则就会执行事务,redis在执行事务时,要么全部执行,要么全部不执行,而且不会被其他客户端打断(原子性),这样就保证了redis事务下数据的一致性。
//测试Redis事务机制
@SuppressWarnings({ "unchecked", "rawtypes" })
@RequestMapping("/multi")
@ResponseBody
public Map<String, Object> textMulti(){
redisTemplate.opsForValue().set("key1", "value1");
List list = (List) redisTemplate.execute((RedisOperations operarions) ->{
//设置要监控key1
operarions.watch("key1");
//开启事务,在exec命令执行之前,全部都只是进入队列
operarions.multi();
operarions.opsForValue().set("key2", "value2");
// operarions.opsForValue().increment("key1", 1);
//获取值为null,因为Redis只是把命令放入队列
Object value2 = operarions.opsForValue().get("key2");
System.out.println("命令在队列,所以value为null【"+value2+"】");
operarions.opsForValue().set("key3", "value3");
Object value3 = operarions.opsForValue().get("key3");
System.out.println("命令在队列,所以value为null【"+value3+"】");
//执行exec命令,将先判别key1是否在监控后被修改过,如果是则不修改事务,否则就执行事务
return operarions.exec();
});
System.out.println("list集合:"+list);
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
redis事务和数据库事务的不同体现在对于redis事务先让命令进入队列,所以一开始他并没有检测这个命令是否能成功,只有在执行exec命令的时候,才能发现错误,对于出错的命令redis只是报出错误,而错误后面的命令依旧被执行。而数据库事务出错后,会报出错误,回滚事务,后面的命令不会被执行
- 使用redis流水线
在默认的情况下,redis客户端是一条条命令发送给redis服务器的,这样显然性能不高。在关系型数据库中,可以使用批量,也就是只有需要执行sql时,才一次性的发送所有的sql去执行。对于redis也是可以的,这就是流水线技术(pipline),在很多情况下并不是redis的性能不佳,而是网络传输造成的瓶颈,使用流水线后就可以大幅提高需要执行很多命令时redis的性能
@RequestMapping("/pipeline")
@ResponseBody
public Map<String, Object> testPipeline(){
long start = System.currentTimeMillis();
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 operations.exec();
});
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start)+"毫秒");
Map<String, Object> map = new HashMap<String, Object>();
map.put("success", true);
return map;
}
在运行如此多的的命令时,需要考虑的另外一个问题就是内存空间的消耗,因为对于程序而言,他最终会返回一个list对象,如果过多的命令执行返回的结果都保存到list中,就会造成内存消耗过大,尤其是在那些高并发的网站中就很容易造成JVM内存溢出的异常,这个时候应该考虑使用迭代的方法执行redis的命令
- 使用redis发布订阅
redis消息监听器
package com.springboot.chapter07.listener;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
@Component
public class RedisMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
// TODO Auto-generated method stub
String body = new String(message.getBody());
String topic = new String(pattern);
System.out.println("消息体:"+body);
System.out.println("渠道名称:"+topic);
}
}
监听redis发布的消息
//注入RedisTemplate
@SuppressWarnings("rawtypes")
@Autowired
private RedisTemplate redisTemplate = null;
//redis连接工厂
@Autowired
private RedisConnectionFactory connectionFactory = null;
//Redis消息监听器
@Autowired
private MessageListener messageListener = null;
//任务池
@Autowired
private ThreadPoolTaskScheduler taskScheduler = null;
/**
* 创建任务池,运行线程等待处理Redis消息
*/
@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(messageListener, topic);
//发送消息
// redisTemplate.convertAndSend("topic2", "123465");
return container;
}
}
在redis客户端输入命令:
publish topicl msg
- 使用Lua脚本
因为redis的计算能力有限,所以为了增强redis的计算能力,在redis2.6版本之后提供了lua脚本支持,而且执行lua脚本的在redis中还具备原子性,所以在需要保证数据一致性的高并发环境中,可以使用lua语言来保证数据的一致性,且lua脚本具备更加强大的运算功能,在高并发需要保证数据一致性时,lua脚本方案比使用redis自身提供的事务更好一些。
在redis中有两种运行lua的方法,一种是直接发送lua 到redis服务器中去执行,另一种是先把lua发送给redis,redis会对lua脚本进行缓存,然后返回一个SHA1的32位编码回来,之后只需发送SHA1和相关参数给redis便可以执行了。
为了支持redis的lua脚本,spring提供了RedisScript接口,与此同时也有一个DefaultRedisScript实现类。
//执行简易Lua脚本
@SuppressWarnings({ "unchecked", "rawtypes" })
@RequestMapping("/lua")
@ResponseBody
public Map<String, Object> testLua(){
DefaultRedisScript<String> rs = new DefaultRedisScript<String>();
//设置脚本
rs.setScriptText("return 'hello redis'");
//定义返回值类型,没有这个定义,spring不会返回结果
rs.setResultType(String.class);
RedisSerializer 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脚本
@SuppressWarnings({ "unchecked" })
@RequestMapping("/lua2")
@ResponseBody
public Map<String, Object> testLua2(String key1,String key2,String value1,String value2){
//定义lua脚本
String str = "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 str == str2 then \n"
+"return 1 \n"
+"end \n"
+"return 0 \n";
System.out.println(str);
DefaultRedisScript<Long> rs = new DefaultRedisScript<Long>();
rs.setScriptText(str);
rs.setResultType(Long.class);
//采用字符串序列化器
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
//定义可以参数
List<String> keyList = new ArrayList<String>();
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;
}
使用Spring缓存注解操作Redis
- 缓存管理器和缓存的启用
#配置redis缓存管理器
spring.cache.type=redis
#缓存名称
spring.cache.cache-names=redisCache
使用**@EnableCaching**驱动spring缓存机制
@SpringBootApplication(scanBasePackages = "com.springboot.chapter07")
@MapperScan(basePackages = "com.springboot.chapter07",annotationClass = Repository.class)
@EnableCaching
public class Chapter07Application {
public static void main(String[] args) {
SpringApplication.run(Chapter07Application.class, args);
}
........
- 开发缓存注解
#数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter07?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
#可以不配置数据库驱动,springboot会自己发现
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#最大等待连接数,设0为没有限制
spring.datasource.tomcat.max-idle=10
#最大等待毫秒数,单位ms,超过时间会出错误信息
spring.datasource.tomcat.max-wait=10000
#数据库连接池初始化连接数
spring.datasource.tomcat.initial-size=5
spring.datasource.tomcat.max-active=50
#日志配置
#logging.level.root=debug
#logging.level.org.springframework=debug
#logging.level.org.org.mybatis=debug
#logging.file=mylog.log
#mybatis映射文件通配
mybatis.mapper-locations=classpath:com/springboot/chapter07/mapper/*.xml
#mybatis扫描别名包,和注解@Alias联用
mybatis.type-aliases-package=com.springboot.chapter07.pojo
#配置typeHandler的扫描包
#mybatis.type-handlers-package=com.springboot.chapter07.typehandler
#1 读未提交
#2 读已提交
#3 可重复读
#4 串行化
#Tomcat数据源默认隔离级别
spring.datasource.tomcat.default-transaction-isolation=2
#bcp2数据库连接池默认隔离级别
#spring.datasource.dbcp2.default-transaction-isolation=2
#Redis配置
#配置连接池属性
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
#配置Redis服务器属性
spring.redis.port=6379
spring.redis.host=192.168.1.103
spring.redis.password=9602242138xq
#Redis连接超时时间,单位毫秒
#spring.redis.timeout=1000
#配置redis缓存管理器
spring.cache.type=redis
spring.cache.cache-names=redisCache
#禁用前缀
#spring.cache.redis.use-key-prefix=false
#允许保留空值
#spring.cache.redis.cache-null-values=true
#自定义前缀
#spring.cache.redis.key-prefix=
#定义超时时间
#spring.cache.redis.time-to-live=600000
pojo
package com.springboot.chapter07.pojo;
import java.io.Serializable;
import org.apache.ibatis.type.Alias;
@Alias("user")
public class User implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer id;
private String user_name;
private String note;
@Override
public String toString() {
return "User [id=" + id + ", user_name=" + user_name + ", note=" + note + "]";
}
}
pojo类实现了Serializable 接口,表示可以被序列化
package com.springboot.chapter07.dao;
import java.util.List;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import com.springboot.chapter07.pojo.User;
@Repository
public interface UserDao {
public User getUser(Integer id);
public int insertUser(User user);
public int updateUser(User user);
public List<User> findUser(@Param("user_name")String user_name,@Param("note")String note);
public int delUser(Integer id);
}
定义用户sql和映射关系
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.chapter07.dao.UserDao">
<select id="getUser" parameterType="Integer" resultType="com.springboot.chapter07.pojo.User">
select id,user_name,note from t_user where id = #{id}
</select>
<insert id="insertUser" parameterType="com.springboot.chapter07.pojo.User" useGeneratedKeys="true" keyProperty="id">
insert into t_user (user_name,note) values (#{user_name},#{note})
</insert>
<update id="updateUser" parameterType="com.springboot.chapter07.pojo.User">
update t_user
<set>
<if test="user_name !=null">user_name = #{user_name},</if>
<if test="note != null">note = #{note}</if>
</set>
where id = #{id}
</update>
<select id="findUser" parameterType="String" resultType="com.springboot.chapter07.pojo.User">
select id,user_name,note from t_user
<where>
<if test="user_name!=null">and user_name = #{user_name}</if>
<if test="note != null">and note =#{note}</if>
</where>
</select>
<delete id="delUser" parameterType="Integer">
delete from t_user where id = #{id}
</delete>
</mapper>
用户服务接口及实现
package com.springboot.chapter07.service;
import java.util.List;
import com.springboot.chapter07.pojo.User;
public interface UserService {
public User getUser(Integer id);
public User insertUser(User user);
public User updateUser(Integer id,String userName);
public List<User> findUser(String userName,String note);
public int delUser(Integer id);
}
package com.springboot.chapter07.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
//获取ID,取参数ID缓存用户
@Transactional
@Override
@Cacheable(value = "redisCache",key = "'redis_user_'+#id")
public User getUser(Integer id) {
// TODO Auto-generated method stub
return userDao.getUser(id);
}
//插入用户,最mybatis会回填ID,取结果ID缓存用户
@Transactional
@Override
@CachePut(value = "redisCache",key = "'redis_user_'+#result.id")
public User insertUser(User user) {
// TODO Auto-generated method stub
userDao.insertUser(user);
return user;
}
//更新数据后,更新缓存,如果condition配置先结果返回null,不缓存
@Transactional
@Override
@CachePut(value = "redisCache",key = "'redis_user_'+#id",condition = "#result!='null'")
public User updateUser(Integer id,String userName) {
// TODO Auto-generated method stub
//此处调用getUser方法,该方法缓存注解失效,所以这里还会执行sql,将查询到数据库最新数据
User user = userDao.getUser(id);//直接从数据库中查,不会从缓存中查
if(user==null) {
return null;
}
user.setUser_name(userName);
userDao.updateUser(user);
return user;
}
//命中率低,不采用缓存机制
@Transactional
@Override
public List<User> findUser(String userName, String note) {
// TODO Auto-generated method stub
return userDao.findUser(userName, note);
}
//移除缓存
@Transactional
@Override
@CacheEvict(value = "redisCahce",key = "'redis_user_'+#id",beforeInvocation = false)
public int delUser(Integer id) {
// TODO Auto-generated method stub
return userDao.delUser(id);
}
}
- @CachePut表示将方法结果返回存放到缓存中
- @Cacheable表示先从缓存中通过定义的键查询,如果可以查询到数据,则返回,否则执行该方法,返回数据,并且将返回的结果保存到缓存中
- @CacheEvict通过定义的缓存键移除缓存,它有一个boolean类型的配置项beforeInvocation,表示在方法之前或之后移除缓存。默认值为false,所以默认为方法之后将缓存移除
用户控制器测试缓存注解
package com.springboot.chapter07.controller;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.springboot.chapter07.pojo.User;
import com.springboot.chapter07.service.UserService;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService = null;
@RequestMapping("/getUser")
@ResponseBody
public User getUser(Integer id) {
return userService.getUser(id);
}
@RequestMapping("/insertUser")
@ResponseBody
public User insertUser(String userName,String note) {
User user = new User();
user.setUser_name(userName);
user.setNote(note);
userService.insertUser(user);
return user;
}
@RequestMapping("/findUsers")
@ResponseBody
public List<User> findUsers (String user_name,String note){
return userService.findUser(user_name, note);
}
@RequestMapping("/updateUser")
@ResponseBody
public Map<String, Object> updateUser(Integer id,String userName){
User user = userService.updateUser(id, userName);
boolean flag =user!=null;
String message = flag?"更新成功":"更新失败";
return resultMap(flag,message);
}
@RequestMapping("/delUser")
@ResponseBody
public Map<String, Object> delUser(Integer id){
int i = userService.delUser(id);
boolean flag = i==1;
String message = flag?"删除成功":"删除失败";
return resultMap(flag, message);
}
private Map<String, Object> resultMap(boolean flag,String message){
Map<String, Object> map = new HashMap<String, Object>();
map.put("flag", flag);
map.put("message", message);
return map;
}
}
从上图可以看到,redis缓存机制会使用#{cacheName}:#{key}的形式作为键保存数据,其次对于这个缓存是永远不超时的
-1表示key存在但没有设置剩余时间,-2表示key不存在
-
缓存注解自调用失效问题
在用户服务接口的实现类中,使用updateUserName方法调用getUser方法中,在getUser方法上的注解将会失效,这是因为spring的缓存机制也是基于springAOP的原理,而在spring中AOP是通过动态代理技术来实现的,这里的updateUserName方法调用getUser方法时类内部的自调用,并不存在代理对象的调用,这样便不会出现aop,也就不会使用到标注在getUser上的缓存注解去获取缓存的值。 -
自定义缓存管理器
在spring中,有两种方法定制缓存管理器,一种是通过配置application.properties文件消除缓存键的前缀和自定义超时时间的属性来定制生成RedisCacheManager;另一种方式是不采用springboot生成的方式,而是完全通过自己的代码创建缓存管理器。
在application.properties文件中配置
#配置redis缓存管理器
spring.cache.type=redis
spring.cache.cache-names=redisCache
#禁用前缀
#spring.cache.redis.use-key-prefix=false
#允许保留空值
#spring.cache.redis.cache-null-values=true
#自定义前缀
#spring.cache.redis.key-prefix=
#定义超时时间
#spring.cache.redis.time-to-live=600000
在springboot的启动类中配置
//自定义redis缓存管理器
@Bean(name = "redisCacheManager")
public RedisCacheManager initRedisCacheManager() {
//redis加锁的写入器
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
//启动redis缓存的默认设置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//设置jdk序列化器
config = config.serializeValuesWith(SerializationPair.fromSerializer(new JdkSerializationRedisSerializer()));
//禁用前缀
config.disableKeyPrefix();
//设置10min超时
config.entryTtl(Duration.ofMinutes(10));
//创建redis缓存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer, config);
return redisCacheManager;
}