Springboot学习笔记(六)——Springboot与Redis的结合

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项目简介

  1. 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。

  1. 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两个接口。

  1. 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的各种数据了

  1. 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

  1. 在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);
	}
  1. 操作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执行的速度

  1. 使用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只是报出错误,而错误后面的命令依旧被执行。而数据库事务出错后,会报出错误,回滚事务,后面的命令不会被执行

  1. 使用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的命令

  1. 使用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

在这里插入图片描述

  1. 使用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

  1. 缓存管理器和缓存的启用
#配置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);
	}
	........
  1. 开发缓存注解
#数据库配置
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不存在

  1. 缓存注解自调用失效问题
    在用户服务接口的实现类中,使用updateUserName方法调用getUser方法中,在getUser方法上的注解将会失效,这是因为spring的缓存机制也是基于springAOP的原理,而在spring中AOP是通过动态代理技术来实现的,这里的updateUserName方法调用getUser方法时类内部的自调用,并不存在代理对象的调用,这样便不会出现aop,也就不会使用到标注在getUser上的缓存注解去获取缓存的值。

  2. 自定义缓存管理器
    在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;
	}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值