SpringBoot整合Redis
本小记学习目标:
1、相关准备知识
2、入门简单项目介绍
3、SpringBoot中配置Redis
4、Redis的特殊用法
5、Spring缓存注解操作Redis
一、相关准备知识
Window环境中Redis的安装
下载Windows版本的Redis,解压
首先在解压后的目录中找到redis.window.conf
添加如下两个配置,以免在后续启动redis的时候内存分配存在异常
maxheap 180MB
maxmemory 120MB
启动redis服务,打开CMD命令窗口,定位到解压目录后,执行如下命令
.\redis-server .\redis.windows.conf
执行命令后提示如下则表示服务端启动成功
The server is now ready to accept connections on port 6379
打开另一个cmd窗口模拟客户端访问服务器,运行如命令
redis-cli -p 6379 -a xiaoxie
注意:这里的xiaoxie,是配置的认证密码,在redis.windows.conf文件中可做如下配置
# 认证密码
requirepass xiaoxie
当客户端运行上面的命令后有如下提示则表示连接成功
127.0.0.1:6379>
Redis相关简介
Redis是一种运行在内存的数据库,它支持7种数据类型的存储【字符串(String)、列表(List)、集合(Set)、Sorted Set(有序集合)、Hash(哈希)、Module(模块)、Streams(流信息)】。
Redis有如下几个关键词
开源、ANSI C语言编写、遵守BSD协议、支持网络、可基于内存、可持久化、日志型、键值数据库、提供API
一般来说应用的查询要远远多于更新,查询与更新的比例大约在9:1到7:3,对于查询比例多的应用,使用Redis可以明显提升性能。
Redis也提供了简单的事务机制来保证在高并发的情况下数据的一致性。
SpringBoot中对于Redis的依赖
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 不依赖Redis的异步客户端lettuce -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Redis客户端驱动jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
注意:对于SpringBoot 2.x版本会引入Lettuce的Redis客户端的驱动,但我们一般常用的会是Jedis,所以可以看到上面的依赖中有<exclusions>元素排除了Lettuce,并且同时引入了jedis的依赖。
二、入门简单项目介绍
Spring提供了一个RedisConnectionFactory接口,通过它可以生成一个RedisConnection接口对象,而生成的这个接口对象则是对Redis底层接口的封装。
Spring中通过RedisConnection接口操作Redis,RedisConnection则对原生的Jedis进行封装。
要获取RedisConnection接口对象,是通过RedisConnectionFactory接口生成的,因而第一步要去配置的就是这个工厂,而配置这个工厂则主要是配置Redis的连接池,对于连接池可以限定其最大连接数、超时时间等属性。
创建RedisConnectionFactory对象,新增类:com.xiaoxie.config.RedisConfig
package com.xiaoxie.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;
@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 rsCfg = connectionFactory.getStandaloneConfiguration();
connectionFactory.setHostName("127.0.0.1");
connectionFactory.setPort(6379);
connectionFactory.setPassword("xiaoxie");
this.connectionFactory = connectionFactory;
return connectionFactory;
}
当我们有了RedisConnectionFactory,通过它则可以创建RedisConnection接口对象。但是当我们在使用时要先从RedisConnectionFactory工厂获取,然后在使用完后还要自行关闭它,Spring中为了简化开发的过程,提供了
RedisTemplate
RedisTemplate首先会从RedisConnectionFactory工厂中获取连接,然后执行对应的Redis命令,最后还会去关闭Redis的连接。这些内容都在这个类中被封装好了。
创建RedisTemplate,在RedisConfig类中新增一个Bean方法
//创建RedisTemplate
@Bean(name = "redisTemplate")
public RedisTemplate<Object,Object> initRedisTemplate(){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
//在这里获取StringRedisSerializer
RedisSerializer stringRedisSerializer = redisTemplate.getStringSerializer();
//设置字符串序列化器
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(stringRedisSerializer);
redisTemplate.setConnectionFactory(initRedisConnectionFactory());
return redisTemplate;
}
这里有一个需要说明的是RedisSerializer
Redis是一种基于字符串存储的NoSQL,然而Java是基于对象的语言,对象是无法直接存储到Redis当中的,这时就会用到Java的序列化机制,当Java类实现了java.io.Serializable接口,就代表类的对象是可以进行序列化的,通过把类对象进行序列化就可以得到二进制字符串,这样Redis就可以把这些类对象以字符串的进行存储。同样Java也可以把那些二进制字符串通过反序列化转换为对象。根据这个原理,Spring提供了序列化器的机制,并且实现了几个序列化
Spring提供了RedisSerializer接口,它有如下两个方法:
serialize:把可以序列化的对象转换为二进制字符串
deserialize:把二进制字符串换为Java对象
它的实现类中有两个重要的类:
StringRedisSerializer
JdkSerializationRedisSerializer(默认)
上面的代码中先获取字符串序列化器再把它设置到需要使用字符串序列化的部分。
Spring对Redis数据类型操作的封装
Spring-data-redis数据类型封装操作接口对照表
操作接口
|
功能
|
获取方式
|
备注
|
GeoOperations
|
地理位置操作
|
redisTemplate.opsForGeo()
|
使用频率低
|
HashOpertions
| 散列操作接口 |
redisTemplate.opsForHash()
| |
HyperLogLogOperations
|
基数操作接口
|
redisTemplate.opsForHyperLogLog()
|
使用频率低
|
ListOperations
|
列表(链表)操作接口
|
redisTemplate.opsForList()
| |
SetOperations
|
集合操作接口
|
redisTemplate.opsForSet()
| |
ValueOperations
|
字符串操作接口
|
redisTemplate.opsForValue()
| |
ZSetOperations
|
有序集合操作接口
|
redisTemplate.opsForZSet()
|
当我们需要对某一个键值做连续操作时,Spring提供了对应的BoundXXXOperations接口
操作接口
|
获取方式
| 说明 |
BoundGeoOperations
|
redisTemplate.boundGeoOps("geo")
|
绑定一个地理位置数据类型的键操作,使用频率低
|
BoundHashOperations
|
redisTemplate.boundHashOps("hash")
|
绑定一个散列数据类型键操作
|
BoundListOperations
|
redisTemplate.boundListOps("list")
|
绑定一个列表(链表)数据类型键操作
|
BoundSetOperations
|
redisTemplate.boundSetOps("set")
|
绑定一个集合数据类型键操作
|
BoundValueOperations
|
redisTemplate.boundValueOps("string")
|
绑定一个字符串集合数据类型的键操作
|
BoundZSetOpertaions
|
redisTemplate.boundZSetOps("zset")
|
绑定一个有序集合数据类型的键操作
|
在SpringBoot的启动类的main方法中添加如下代码,则可以向redis中写入对应的值
package com.xiaoxie;
import com.xiaoxie.config.RedisConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootApplication
public class SpringBootStartApplication {
public static void main(String[] args) {
System.out.println("RedisTemplate测试:");
ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");
redisTemplate.opsForValue().set("key1","value1");
redisTemplate.opsForHash().put("hash","field","hvalue");
System.out.println("RedisTemplate测试完成,写入相应的值");
SpringApplication.run(SpringBootStartApplication.class,args);
}
}
SessionCallback和RedisCallback接口
这两个接口的作用是让RedisTemplate进行回调,通过它们可以在同一条连接下执行多个Redis命令。
这两个接口SessionCallback提供的封装良好,使用率更高。
修改main方法中执行Redis命名的过程,使用RedisCallback接口和SessionCallback来同时执行两个写入值的命令
package com.xiaoxie;
import com.xiaoxie.config.RedisConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
@SpringBootApplication
public class SpringBootStartApplication {
public static void main(String[] args) {
System.out.println("RedisTemplate测试:");
ApplicationContext ctx = new AnnotationConfigApplicationContext(RedisConfig.class);
RedisTemplate redisTemplate = (RedisTemplate) ctx.getBean("redisTemplate");
/*redisTemplate.opsForValue().set("key1","value1");
redisTemplate.opsForHash().put("hash","field","hvalue");*/
//如下使用RedisCallback进行回调
/*redisTemplate.execute(new RedisCallback() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
connection.set("key1".getBytes(),"RedisCallback".getBytes());
connection.hSet("hash".getBytes(),"field".getBytes(),"hvalue".getBytes());
return null;
}
});*/
//使用SessionCallback进行回调
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.opsForValue().set("key1","SessionCallback");
operations.opsForHash().put("hash","field","hvalue");
return null;
}
});
System.out.println("RedisTemplate测试完成,写入相应的值");
SpringApplication.run(SpringBootStartApplication.class,args);
}
}
上面的过程使用了匿名灯的方式去使用它们,RedisCallback接口不是那么的友好,但是它可以修改一些底层的东西,SessionCallback相对来说友好是大部分情况下推荐使用的接口。
三、Spirng Boot中配置Redis
Redis的配置集成
Spring Boot中集成Redis,在配置文件中配置Redis服务器,在application.properties文件中新增如下内容
#配置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=2000ms
#配置Redis服务器
spring.redis.port=6379
spring.redis.host=127.0.0.1
spring.redis.password=xiaoxie
#Redis连接超时时间,单位毫秒
spring.redis.timeout=1000ms
对于Redis的配置不需要使用单独的配置类进行编码进行,直接在SpringBoot的启动类中添加如下代码
@Autowired
private RedisTemplate redisTemplate = null;
//自定义后初始化方法
@PostConstruct
public void init(){
initRedisTemplate();
}
//设置RedisTemplate的序列化器
private void initRedisTemplate() {
RedisSerializer stringSerializer = redisTemplate.getStringSerializer();
redisTemplate.setKeySerializer(stringSerializer);
redisTemplate.setValueSerializer(stringSerializer);
redisTemplate.setHashKeySerializer(stringSerializer);
}
上面的代码首先通过@Autowired注入由Spring Boot根据配置文件生成的RedisTemplate对象,然后利用Spring Bean生命周期的特性使用注解@PostConstruct自己定义后初始化方法,在这个自定义的初始化方法中对序列化器进行修改。
在原来TestController的controller类中新增controller的方法如下
@Autowired
RedisTemplate redisTemplate = null;
@RequestMapping("/redis")
@ResponseBody
public String testRedis(){
//使用SessionCallback进行回调
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.opsForValue().set("key1","SessionCallback_haha");
operations.opsForHash().put("hash","field","hvalue");
return null;
}
});
return "执行完成!";
}
运行启动类,并在浏览器中访问 http://localhost:8080/redis,页面会显示“执行完成!”,使用客户端执行redis命令:get key1
得到的结果是SessionCallback_haha
操作Redis数据类型
1、操作字条串和散列
新增一个controller类com.xiaoxie.controller.RedisController
package com.xiaoxie.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import redis.clients.jedis.Jedis;
import java.util.HashMap;
import java.util.Map;
@Controller
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate redisTemplate = null;
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
@RequestMapping("/stringAndHash")
@ResponseBody
public Map<String,Object> StringAndHash(){
redisTemplate.opsForValue().set("key1","value1");
redisTemplate.opsForValue().set("int_key","1"); //这里使用的是JDK序列化器,写入的1不是整数,不可以运算
stringRedisTemplate.opsForValue().set("int","1");
//使用运算
stringRedisTemplate.opsForValue().increment("int",2);
//获取底层Jedis连接
Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
//减 1操作,这个命令RedisTemplate不支持,需要先获取底层连接再操作
jedis.decr("int");
Map<String,String> hash = new HashMap<>();
hash.put("field1","value1");
hash.put("field2","value2");
//存入一个散列数据类型
stringRedisTemplate.opsForHash().putAll("hash",hash);
//新增一个字段
stringRedisTemplate.opsForHash().put("hash","field3","value3");
//绑定散列操作的key
BoundHashOperations hashOperations = stringRedisTemplate.boundHashOps("hash");
//删除两个字段
hashOperations.delete("field1","field2");
//新增一个字段
hashOperations.put("field4","value4");
Map<String,Object> map = new HashMap<>();
map.put("success",true);
return map;
}
}
这里自动注入了RedisTemplate和StringRedisTemplate对象。
先用RedisTemplate存入了key1,int_key,由于使用的是JDK序列化器,所以在Redis服务器中int_key它不是整数,而是一个被JDK序列化后的二进制字符串,它是没有办法使用Redis命令进行运算的,为了解决这个问题,使用StringRedisTemplate对象保存了一个键为"int"的整数,这个子就可进行接下来的运算了。
注意:RedisTemplate是不能支持底层所有的Redis命令的,所以在需要使用底层Redis命令的地方,先获取到原始的Redis连接的Jedis对象:Jedis jedis = (Jedis) stringRedisTemplate.getConnectionFactory().getConnection().getNativeConnection();
对于散列的操作,可以看到先生成一个Map,然后使用putAll方式插入多个散列,同时也使用BoundHashOperations来操作同一数据。
2、操作链表
在controller的类中新增一个方法用来对list链表进行操作
@RequestMapping("/list")
@ResponseBody
public Map<String,Object> RedisList(){
//判断是否存在key,如果存在则删除
if(stringRedisTemplate.hasKey("list1")){
stringRedisTemplate.delete("list1");
}
if(stringRedisTemplate.hasKey("list2")){
stringRedisTemplate.delete("list2");
}
//插入两个列表
//从左到右顺序是: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 listOperations = stringRedisTemplate.boundListOps("list2");
//从右边弹出一个成员
Object result1 = listOperations.rightPop();
//获取定位元素,Redis从0开始计算
Object result2 = listOperations.index(1);
//从左边插入链表
listOperations.leftPush("v0");
//求链表长度
Long size = listOperations.size();
//求链表下标区间成员,整个链表下标范围是0~size-1
List elements = listOperations.range(0,size-2);
Map<String,Object> map = new HashMap<>();
map.put("success",true);
map.put("list",elements);
map.put("list_size",elements.size());
return map;
}
这里有如下两个问题需要注意:
1、列表元素的顺序问题,是从左到右还是从右到左,这个要注意
2、下标问题,在Redis中是以0开始的
操作集合
在controller的类中新增一个方法来新增对set集合的操作
@RequestMapping("/set")
@ResponseBody
public Map<String,Object> RedisSet(){
Map<String,Object> map = new HashMap();
if(stringRedisTemplate.hasKey("set1")){
stringRedisTemplate.delete("set1");
}
if(stringRedisTemplate.hasKey("set2")){
stringRedisTemplate.delete("set2");
}
if(stringRedisTemplate.hasKey("inter")){
stringRedisTemplate.delete("inter");
}
if(stringRedisTemplate.hasKey("diff")){
stringRedisTemplate.delete("diff");
}
if(stringRedisTemplate.hasKey("union")){
stringRedisTemplate.delete("union");
}
stringRedisTemplate.opsForSet().add("set1","v1","v2","v1","v3","v4","v5");
stringRedisTemplate.opsForSet().add("set2","v2","v4","v6","v8");
//绑定set1集合
BoundSetOperations setOperations = stringRedisTemplate.boundSetOps("set1");
//增加两个元素
setOperations.add("v6","v7");
//删除三个元素
setOperations.remove("v0","v1","v7");
Set set1 = setOperations.members();
Set set2 = stringRedisTemplate.boundSetOps("set2").members();
map.put("set1集合",set1);
map.put("set2集合",set2);
//求交集
Set inter = setOperations.intersect("set2");
map.put("set1与set2的交集",inter);
//求交集并用新集合inter保存
setOperations.intersectAndStore("set2","inter");
//求差集
Set diff = setOperations.diff("set2");
map.put("set1与set2的差集",diff);
//求差集并用新集合diff保存
setOperations.diffAndStore("set2","diff");
//求并集
Set union = setOperations.union("set2");
map.put("set1与set2的并集",union);
//求并集并用新集合union保存
setOperations.unionAndStore("set2","union");
map.put("success",true);
return map;
}
注意:对于集合set,在Redis中是不允许成员重复的,它在数据结构上是一个散列表的结构,所以它是元序的。
上面的集合是无序的,为了让集合支持排序,Redis提供了有序集合zset,它与无序的集合差异不大,也是一种散列表存储的方式,同时它的有序性靠的是它在数据结构中增加一个属性score得以支持。
Spring提供了TypedTuple接口,并且还提供了它的默认实现类DefalutTypedTuple
在TypedTuple接口中,value保存的是有序集合的值,score保存的是分数,Redis使用分类来完成集合的排序。
在controller中新增方法来操作zset集合
@RequestMapping("/zset")
@ResponseBody
public Map<String,Object> RedisZset(){
if(stringRedisTemplate.hasKey("zset1")){
stringRedisTemplate.delete("zset1");
}
Map<String,Object> map = new HashMap<>();
Set<ZSetOperations.TypedTuple<String>> typedTupleSet = new HashSet<>();
for(int i=0;i<=9;i++){
//分数
double score = i * 0.1;
//创建一个TypedTuple对象,并存入值和分数
ZSetOperations.TypedTuple<String> typedTuple = new DefaultTypedTuple<>("value" + i,score);
typedTupleSet.add(typedTuple);
}
//向有序集合插入元素
stringRedisTemplate.opsForZSet().add("zset1",typedTupleSet);
//绑定zset1
BoundZSetOperations<String,String> zSetOperations = stringRedisTemplate.boundZSetOps("zset1");
//增加一个元素
zSetOperations.add("value10",0.25);
Set<String> setRange = zSetOperations.range(1,6);
map.put("排序1~6的集合",setRange);
Set<String> setScore = zSetOperations.rangeByScore(0.2,0.6);
map.put("分数在0.2到0.6的集合",setScore);
//定义值范围
RedisZSetCommands.Range range = new RedisZSetCommands.Range();
range.gt("value3");//大于value3
range.lte("value8");//小于等于value8
Set<String> setLex = zSetOperations.rangeByLex(range);
map.put("大于value3小于等于value8的集合",setLex);
//删除元素
zSetOperations.remove("value9","value2");
//求分数
Double score = zSetOperations.score("value8");
map.put("value8的分数",score);
//在下标区间下,按分数排序,时同返回value和score
Set<ZSetOperations.TypedTuple<String>> rangeSet = zSetOperations.rangeWithScores(1,6);
map.put("rangeWithScores(1,6)",rangeSet);
Set<ZSetOperations.TypedTuple<String>> scoreSet = zSetOperations.rangeByScoreWithScores(1,6);
map.put("rangeByScoreWithScores(1,6)",scoreSet);
//按从大到小排序
Set<String> reverseSet = zSetOperations.reverseRange(2,8);
map.put("reverseRange(2,8)",reverseSet);
map.put("success",true);
return map;
}
TypedTuple保存有序集合的元素,默认情况下,有序集合是从小到大排序的。
四、Redis的特殊用法
Redis的一些常用的特殊用法:事务支持、流水线、发布订阅、Lua脚本语言 ……
Redis事务
Redis是支持一定事务能力的NoSQL,在Redis中使用事务,通常的命令组合是watch... multi... exec,需要在Redis连接中执行多个命令,这时则可以考虑使用SessionCallback接口来实现。
watch命令可以监控Redis的一些键,multi命令用来开始事务,当事务开始后,客户端的命令不会马上执行而是会存放在一个队列中(这里调用Redis命令,结果都会返回null),exec命令用于执行事务,它在队列执行前会判断被watch监控的Redis的键数据是否发生过变化(注意:就算是赋予了与之前相同的值也会被认为是变化过的),如果它认为发生了变化,那么Redis会取消事务,否则就会执行事务,Redis在执行事务时,要么是全部执行,要么是全部不执行,而不会被其他客户端打断,这样就保证了Redis事务下数据的一致性。
在controller类中新增一个事务相关的验证方法
@RequestMapping("/multi")
@ResponseBody
public Map<String,Object> RedisMulti(){
redisTemplate.opsForValue().set("key1","v1");
List list = (List) redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
//设置要监控的键
operations.watch("key1");
//开启事务
operations.multi();
operations.opsForValue().set("key2","v2");
//这里获取值返回为null
Object v2 = operations.opsForValue().get("key2");
System.out.println("由于命令还在队列中,返回的值为null,key2-->" + v2 );
operations.opsForValue().set("key3","v3");
Object v3 = operations.opsForValue().get("key3");
System.out.println("由于命令还在队列中,返回的值为null,key3-->" + v3 );
//执行(监控的键未变化过)或取消事务(监控的键变化过)
return operations.exec();
}
});
System.out.println("执行事务返回结果:" + list);
Map<String,Object> map = new HashMap<>();
map.put("success",true);
return map;
}
Redis流水线
在默认情况下,Redis客户端是一条一条命令发送给Redis服务器的,这们性能不是很好,为了能达到批量执行命令的目的,Redis中提供了流水线pipline技术,这样就可以大幅度地提升Redis的性能。
在controller类中新增一个方法来测试Redis流水线方式读写的性能
@RequestMapping("/pipeline")
@ResponseBody
public Map<String,Object> redisPipeline(){
Long start = System.currentTimeMillis(); //开始毫秒值
List list = redisTemplate.executePipelined(new SessionCallback(){
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
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<>();
map.put("success",true);
map.put("执行耗时(毫秒)",(end-start));
return map;
}
注意,上面只是做的测试,在实际开发过程中需要同时考虑一个问题就是内存空间的消耗问题,对于程序它会返回一个List对象,如果过多的命令执行返回结果都会保存到这个List当中,这样就会造成内存消耗过大,特别是一些高并发的网站很容易导致JVM内存溢出。
第二要注意的点就是,使用流水线的过程中,所有命令也只是进入队例中没有地,所以执行命令返回值也是空的。
Redis发布订阅
发布订阅是消息的一种常用模式。
模式:Redis提供一个渠道,把消息发送到这个渠道上,多个系统云监听这个渠道。
当一条消息发送到渠道,渠道会通知它的监听者,那么监听者则可以得到这个渠道给它们的消息了,之后监听者则根据需要去处理消息。
为了能接收Redis渠道发送过来的消息,先要定义一个消息监听器(MessageListener)
新增一个类:com.xiaoxie.listener.RedisMessageListener
package com.xiaoxie.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) {
//消息体
String body = new String(message.getBody());
//渠道名称
String topic = new String(pattern);
System.out.println(body);
System.out.println(topic);
}
}
RedisMessageListener类实现了接口MessageListener
方法onMessage是得到消息后的处理方法,方法中的message参数代表了Redis发送过来的消息,pattern是指的渠道名称
这个类使用了注解@Component,这样的话在Spring Boot扫描后,会把它自动装配到IoC容器中。
在Spring Boot的启动类中配置其它的信息使得系统可以监控Redis消息,修改后的内容如下:
@SpringBootApplication
public class SpringBootStartApplication {
@Autowired
private RedisTemplate redisTemplate = null;
//Redis连接工厂
@Autowired
private RedisConnectionFactory connectionFactory = null;
//Redis消息监听器
@Autowired
private MessageListener redisMessageListener = null;
//任务池
private ThreadPoolTaskScheduler taskScheduler = null;
/**
* 创建任务池,运行线程等待处理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;
}
//自定义后初始化方法
@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(SpringBootStartApplication.class,args);
}
}
在controller类中新增一个方法用来发布消息
@RequestMapping("/sendMessage")
@ResponseBody
public Map<String,Object> sendMessage(String topic,String msg){
Map<String,Object> map = new HashMap<>();
stringRedisTemplate.convertAndSend(topic,msg);
map.put("success",true);
return map;
}
这里要注意一下,在访问时需要同时提供两个参数topic(表示指定渠道),msg(表示消息内容)
在客户端中使用下面的命令出可以发布消息:publish topic1 msg
使用Lua脚本
Redis中有很多命令,但是Redis提供的计算能力是比较有限的,为了增强Redis的计算能力,Redis在2.6版本后提供了Lua脚本的支持,而且执行lua脚本在Redis中还具备有原子性,所以在要保证数据一致性的高并发环境中,也可以使用Redis的Lua语言来保证数据的一致性。
Redis中两种运行Lua的方法
方法一、直接发送Lua到Redis服务器去执行
方法二、把Lua发送给一Redis,Redis会对Lua脚本进行缓存,然后返回一个SHA1的32位编码回来,之后只需要发送SHA1和相关参数给Redis即可。
方法二中使用32位编码进行执行的方法目的是为了在让长的Lua脚本在执行过程中不会导致网络传输瓶颈,从而提高系统性能。
为了支持Redis的Lua脚本,Spring提供了RedisScript接口,同时也有一个DefaultRedisScript实现类
在controller类中新增一个方法来执行一个简单的Lua脚本
@RequestMapping("/lua")
@ResponseBody
public Map<String,Object> redisLua(){
Map<String,Object> map = new HashMap<>();
DefaultRedisScript<String> rs = new DefaultRedisScript<>();
//设置脚本
rs.setScriptText("return 'OK,Redis!'");
//定义返回类型,如果不定义Spring不返回结果
rs.setResultType(String.class);
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
//执行lua脚本
String str = (String) redisTemplate.execute(rs,stringRedisSerializer,stringRedisSerializer,null);
System.out.println(str);
String sha1 = rs.getSha1();
System.out.println(sha1);
map.put("success",true);
return map;
}
上面的Lua脚本只是简单的返回一个字符串,如果要执行复杂的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
return 1
end
return 0
这个脚本使用两个键去保存两个值,然后获取对应的值进行比较,如果比较相等则返回1,否则返回0
KEYS[1]、KEYS[2]表示客户端传递的第一个键和第二个键
ARGV[1]、ARGV[2]表示客户端传递的第一个值和第二个值
在controller类中新增一个方法进行对复杂Lua的测试
@RequestMapping("/lua2")
@ResponseBody
public Map<String,Object> redisLua2(String key1,String key2,String value1,String value2){
Map<String,Object> map = new HashMap<>();
//定义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<>();
rs.setScriptText(lua);
rs.setResultType(Long.class);
//采用字符串序列化器
RedisSerializer<String> stringRedisSerializer = redisTemplate.getStringSerializer();
//定义key参数
List<String> keyList = new ArrayList<>();
keyList.add(key1);
keyList.add(key2);
//传递两个参数值,其中第一个序列化器是key的序列化器,第二个序列化器是参数的序列化器
Long result = (Long) redisTemplate.execute(rs,stringRedisSerializer,stringRedisSerializer,keyList,value1,value2);
map.put("result",result);
map.put("sha1",rs.getSha1());
return map;
}
五、Spring缓存注解操作Redis
Spring提供缓存注解去操作Redis的目的是为了简化编程过程
在Spring使用缓存注解前,需要配置缓存管理器
缓存管理器的接口:CacheManager及相关的类,对于Redis则主要的类是:RedisCacheManager
同步我们引入mybatis、mysql、druid支持,配置redis缓存管理器
数据库中新键一个t_user的表
CREATE TABLE `t_user` (
`id` int(12) NOT NULL AUTO_INCREMENT,
`user_name` varchar(60) NOT NULL,
`note` varchar(512) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8;
表中可以随意新增多条测试记录
在application.properties文件中我们新增如下配置
#durid的配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=org.gjt.mm.mysql.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=root
#连接池的属性
spring.datasource.druid.initial-size=15
spring.datasource.druid.max-active=100
spring.datasource.druid.min-idle=15
spring.datasource.druid.max-wait=60000
spring.datasource.druid.keep-alive=true
#数据源默认的隔离级别"读写提交"
spring.datasource.druid.default-transaction-isolation=2
#mybatis配置
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.xiaoxie.pojo
#日志配置为DEBUG级别
logging.level.root=DEBUG
logging.level.org.springframework=DEBUG
logging.level.org.org.mybatis=DEBUG
#Redis缓存管理器配置
spring.cache.type=REDIS
spring.cache.cache-names=redisCache
新增实体类:com.xiaoxie.pojo.User
package com.xiaoxie.pojo;
import org.apache.ibatis.type.Alias;
import java.io.Serializable;
@Alias("user")
public class User implements Serializable {
private Long id;
private String userName;
private String note;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getNote() {
return note;
}
public void setNote(String note) {
this.note = note;
}
}
新增一个dao接口:com.xiaoxie.dao.UserDao
package com.xiaoxie.dao;
import com.xiaoxie.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
@Component
public interface UserDao {
/**
* 获取单个用户
* @param id
* @return
*/
User getUser(Long id);
/**
* 保存用户
* @param user
* @return
*/
int insertUser(User user);
/**
* 修改用户
* @param user
* @return
*/
int updateUser(User user);
/**
* 查找用户
* @param userName
* @param note
* @return
*/
List<User> findUsers(@Param("userName") String userName,@Param("note") String note);
/**
* 删除用户
* @param id
* @return
*/
int deleteUser(Long id);
}
配置对应的mybatis的xml配置文件,根据application.properties的配置我们在resources/mybatis/mapper下新增一个配置文件userMapper.xml
<?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.xiaoxie.dao.UserDao">
<select id="getUser" parameterType="long" resultType="user">
select id,user_name as userName,note from t_user where id=#{id}
</select>
<insert id="insertUser" useGeneratedKeys="true" keyProperty="id" parameterType="user">
insert into t_user(user_name,note) VALUES (#{userName},#{note})
</insert>
<update id="updateUser">
update t_user
<set>
<if test="userName!=null">user_name=#{userName},</if>
<if test="note!=null">note=#{note}</if>
</set>
where id=#{id}
</update>
<select id="findUsers" resultType="user">
select id,user_name as userName,note from t_user
<where>
<if test="userName != null">
and user_name=#{userName}
</if>
<if test="note!=null">
and note=#{note}
</if>
</where>
</select>
<delete id="deleteUser" parameterType="long">
DELETE from t_user where id=#{id}
</delete>
</mapper>
新增service接口:com.xiaoxie.service.UserService
package com.xiaoxie.service;
import com.xiaoxie.pojo.User;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface UserService {
/**
* 获取单个用户
* @param id
* @return
*/
User getUser(Long id);
/**
* 保存用户
* @param user
* @return
*/
User insertUser(User user);
/**
* 修改用户
* @param id
* @param userName
* @return
*/
User updateUserName(Long id ,String userName);
/**
* 查找用户
* @param userName
* @param note
* @return
*/
List<User> findUsers(String userName, String note);
/**
* 删除用户
* @param id
* @return
*/
int deleteUser(Long id);
}
新增service实现类:com.xiaoxie.service.UserServiceImpl
package com.xiaoxie.service;
import com.xiaoxie.dao.UserDao;
import com.xiaoxie.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao = null;
@Override
@Transactional //开启事务
@Cacheable(value = "redisCache",key = "'redis_user_'+#id")
public User getUser(Long id) {
return userDao.getUser(id);
}
@Override
@Transactional
@CachePut(value = "redisCache",key = "'redis_user_'+#result.id")
public User insertUser(User user) {
userDao.insertUser(user);
return user;
}
@Override
@Transactional
@CachePut(value = "redisCache",condition = "#result!='null'",key ="'redis_user_' + #id")
public User updateUserName(Long id, String userName) {
//在这里调用getUser,这个方法的缓存注解失效
User user = this.getUser(id); //这里不会走缓存,查询数据库
if(user == null){
return null;
}
user.setUserName(userName);
userDao.updateUser(user);
return user;
}
//这里命中率低,不采用缓存机制
@Override
@Transactional
public List<User> findUsers(String userName, String note) {
return userDao.findUsers(userName,note);
}
//移除缓存
@Override
@Transactional
@CacheEvict(value = "redisCache",key ="'redis_user_' + #id",beforeInvocation = false)
public int deleteUser(Long id) {
return userDao.deleteUser(id);
}
}
关于上面的Service,在方法上我们看到有如下几个注解是关于缓存的
@CachePut:表示把方法结果返回存放到缓存中
@Cacheable:表示从缓存中通过定义的键查询,如果可以查询到数据则返回,否则执行这个方法返回数据,并把数据结果保存到缓存中
@CacheEvict:通过定义的键移动缓存,beforeInvocation配置项可以配置一个boolean值,表示在方法之前或者之后移除缓存,默认值为false(在方法之后把缓存移除)
在上面方法中的注解都配置了value="redisCache",它是表示引用Spring Boot中配置的“redisCache”这个缓存
键配置项key配置的值使用了Spring EL表达式,如key = "'redis_user_' + #id",这其中的#id表示参数,它通过参数名称来做匹配,有这样的配置则要求方法中存在一个名称为id的参数。而在insertUser方法中我们看到key的值如下:key = "'redis_user_'+#result.id",这里的#result表示返回的结果对象,#result.id则表示取出返回结果对象中的id属性值。
updateUserName方法里先调用了getUser方法(同一个service中的方法),则不会使用缓存,这也是合理的,对于更新这种操作直接使用缓存是有风险的所有尽量要从数据库中去查询最新的数据再做操作。
findUsers方法,这个方法没有使用缓存,它不使用缓存的原因是因为随着用户的查询条件的变化,会导致命中率很低,这个时候使用缓存除了消耗资源外对系统性能的提升不会有大的改进。
新增一个controller类:com.xiaoxie.controller.UserController,同时新增getUser方法
package com.xiaoxie.controller;
import com.xiaoxie.pojo.User;
import com.xiaoxie.service.UserService;
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;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService = null;
@RequestMapping("/getUser")
@ResponseBody
public User getUser(Long id){
return userService.getUser(id);
}
}
修改SpringBoot的启动类,让它支持缓存同时mybatis的扫描配置
在类上添加如下两个注解
@EnableCaching
@MapperScan(basePackages = "com.xiaoxie.dao",annotationClass = Mapper.class)
运行SpringBoot的启动类,在浏览器中访问地址:http://localhost:8080/user/gtUser?id=2
controller中会调用service中的getUser方法,会获取User并在redisCache中做缓存
在redis客户端中运行命令:keys *
返回的结果:"redisCache::redis_user_2"
如上的返回结果表示已经把查询返回的结果在redis中缓存起来了。
在UserController类中新增方法insertUser
@RequestMapping("/insertUser")
@ResponseBody
public User insertUser(String userName,String note){
User user = new User();
user.setUserName(userName);
user.setNote(note);
userService.insertUser(user);
return user;
}
运行SpringBoot的启动类,在浏览器中访问地址:http://localhost:8080/user/insertUser?userName=赵云¬e=常山赵子龙
在redis客户端中运行命令:keys *
返回的结果:
1) "redisCache::redis_user_2"
2) "redisCache::redis_user_24"
在UserController类中新增方法findUsers
@RequestMapping("/findUsers")
@ResponseBody
public List<User> findUsers(String userName,String note){
return userService.findUsers(userName,note);
}
运行SpringBoot的启动类,在浏览器中访问地址:http://localhost:8080/user/findUsers?userName=王五¬e=来啊
运行完成后在redis客户端中运行命令:keys *
返回的结果:
1) "redisCache::redis_user_2"
2) "redisCache::redis_user_24"
从上面返回的结果可以看出并没有在redis中新增缓存内容
在UserController类中新增方法updateUserName方法
@RequestMapping("/updateUserName")
@ResponseBody
public Map<String,Object> updateUserName(Long id,String userName){
Map<String,Object> map = new HashMap<>();
User user = userService.updateUserName(id, userName);
boolean flag = user != null;
String message = flag?"更新成功":"更新失败";
map.put("success",flag);
map.put("message",message);
return map;
}
运行SpringBoot的启动类,在浏览器中访问地址:http://localhost:8080/user/updateUserName?id=10&UserName=赵六六
运行完成后在redis客户端中运行客户:keys *
返回的结果如下:
1) "redisCache::redis_user_2"
2) "redisCache::redis_user_10"
3) "redisCache::redis_user_24"
从上面返回的结果可以看出来,被更新的那个记录已经被redis缓存起来了
在UserController类中新增方法deleteUser方法
@RequestMapping("/deleteUser")
@ResponseBody
public Map<String,Object> deleteUser(Long id){
int result = userService.deleteUser(id);
boolean flag = result==1;
String message = flag?"删除成功":"删除失败";
Map<String,Object> map = new HashMap<>();
map.put("success",flag);
map.put("message",message);
return map;
}
脏数据问题
脏数据指的是缓存中的数据与实现数据库中的数据不一致的情况。
一般来说,对于读操作是可以允许非实时的数据的,它与实时数据可以存在一定的延迟,但是一个脏数据一致存在则就会造成失真严重的问题。要解决这个问题一般的做法是规定一个时间,让缓存失效,在Redis中也可以设置超时的时间,当缓存超过超时时间后,则应用不再可以从缓存中获了数据,只能从数据库中重新获取最新的数据,如果系统对于实时性要求比较高则可以把缓存时间设置小一些。但是这样会加大缓存的刷新时间,增加数据库压力。
对于写操作,要特别谨慎,考虑从数据库中先读取最新数据,然后再更新数据,避免将缓存的脏数据写入数据库中,从而导致出现业务问题。
自定义缓存管理器
Spring中有两种自定义缓存管理器
1、通过修改配置文件中的配置来自定义
2、不使用Spring Boot自己生成的方式而使用自己的代码来创建缓存管理器(当需要比较多的自己定义时,推荐使用)
通过修改配置文人年中的配置来自定义
修改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=XX
#定义超时时间,单位是毫秒
spring.cache.redis.time-to-live=60000ms
这个时候我们再去getUser,写到redis缓存中的键则没有了redisCache::这个前缀了,而且新生成的键是存在超时时间的,上面的配置是60秒,超过这个时间缓存则会清除掉。
自定义缓存管理器
新增一个config类:com.xiaoxie.config.RedisCacheConfig
package com.xiaoxie.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
@Configuration
public class RedisCacheConfig {
//注入连接工厂,由Spring Boot自动配置生成
@Autowired
private RedisConnectionFactory connectionFactory = null;
//自定义Redis缓存管理器
@Bean(name = "redisCacheManager")
public RedisCacheManager initRedisCacheManage(){
//Redis加锁的写入器
RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
//启用Redis缓存的默认设置
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
//设置JDK序列化器
configuration = configuration.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(
new JdkSerializationRedisSerializer()
));
//禁用前缀
configuration = configuration.disableKeyPrefix();
//设置1分钟超时
configuration = configuration.entryTtl(Duration.ofMinutes(1));
//创建Redis缓存管理器
RedisCacheManager redisCacheManager = new RedisCacheManager(writer,configuration);
return redisCacheManager;
}
}
这个配置类的目的就是返回一个Bean的名称为redisCacheManager的缓存管理器
把配置文件的对缓存管理器的配置注释掉,使用getUser进行测发现,自定义的缓存管理器一样是生效的。