Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache

目录:
Redis从入门到精通(一):缓存
Redis从入门到精通(二):分布式锁以及缓存相关问题
Redis从入门到精通(三):Redis持久化算法以及内存淘汰策略
Redis从入门到精通(四):Redis常用数据结构以及指令
Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
Redis从入门到精通(六):Redis高可用原理
Redis从入门到精通(七):跳跃表的简介与实现

java客户端

在redis中,适配了许多语言,如java,python,nodejs等。我们是java开发,就使用java的redis客户端。

redis有两种客户端供java使用:jedes, lettuce

jedis:

  1. 直连模式,多线程中不安全,使用连接池进行处理连接。
  2. 命令更加全面
  3. 暴露底层API
  4. 不支持异步操作,阻塞I/O

lettuce:

  1. 线程安全,可以异步
  2. 基于Netty的事件驱动

我们现在用的都是lettuce版本的,因此这里选用jdk1.8+lettuce+idea进行整合。

pom.xml

Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache

Redis.template

Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
我这里是报错了的,因为连接问题。

解决方法:

  1. 关闭防火墙或者设置安全组
  2. 更改redis.conf文件
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache

当然,也可以自己写配置文件,不改人家的。

我们看一下RedisTemplate这个类:
Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
这些方法其实和我们之前说的各种基本方法类似,只不过封装了函数而已。而我们还有一个类是StringRedisTemplate,这个类实际上是继承了RedisTemplate的。
Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
但是我们看二者的序列化方式:
Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
可以看出来,StringRedisTemplate使用了String类的序列化方式,而RedisTemplate利用的是JDK本身的序列化方式。而我们序列化的方式不同,造就了一些使用或者特性的不同。

  1. S与R的数据不互通。由于序列化方式不同,相同key的二进制代码不同,因此存入redis的key不同,造就了数据不互通。
  2. 由于序列化方式不同,因此效率不同。JDK原带的序列化方式效率低下。
  3. JDK序列化可能会产生乱码,且需要实现Serializable接口

所以可以看到,我们最好不用原生的JDK序列化方式,在使用Redis做序列化的时候,我们可以使用多种方法代替原生序列化。

  1. 简单的k-v使用StringRedisTemplate替代
  2. 使用Jimport com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisSerializableConfiguration { @Bean public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //使用该类替换原生序列化方式 Jackson2JsonRedisSerializer redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); //序列化转换器 ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); redisSerializer.setObjectMapper(objectMapper); //设置key-value序列化规则 redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(redisSerializer); //设置hash key-value序列化规则 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(redisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }

    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache

RedisTemplate连接池

Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
这里就不说Jedis怎么配置了,毕竟不怎么用。

Redis+Lua分布式锁

  1. 为什么要用脚本语言Lua?
    先看一个伪代码例子:

    addLockOpertaion(){ 	String key = "key"; 	if(setnx(key, "value") == 1){  //如果key没有上锁,设置锁 		expire(key, 30000, TimeUnit.MILLISECONDS); //过期时间30s 		try{ 			//上锁后要进行的操作 		}catch(Exception e){ 			e.printStackTrace(); 		}finally{ 			del(key);    //解锁 		} 	}else{ 		Thread.sleep(100); 		addLockOperation(); //自旋调用 	} } 

    这代码是有问题的,比如当进行上锁后的操作后,突然程序宕机,解锁命令无法执行,造成死锁。怎么办?

    方法还是有的:加上上锁时间,比如30s,30s后直接解锁,不管程序有没有完成。

    但是这种方法也有问题:假设程序在第40s完成,而锁在30s时释放,那么另一个程序进来拿到锁并且开始运行,新进程序执行时间大于10s,在第40s的时候,新进程序的锁被老程序释放了!画一个时间轴理解一下:
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
    这种问题其实也有解决方法,设置唯一key-value,只能对本key-value进行解锁。具体方法就是key加上本进程号就能解决。

    addLockOpertaion(){ 	String key = "key"; 	String value = Thread.currentThread.getId(); 	if(setnx(key, value) == 1){  //如果key没有上锁,设置锁 		expire(key, 30000, TimeUnit.MILLISECONDS); //过期时间30s 		try{ 			//上锁后要进行的操作 		}catch(Exception e){ 			e.printStackTrace(); 		}finally{ 			if(get(key).equals(value)) 				del(key);    //解锁 		} 	}else{ 		Thread.sleep(100); 		addLockOperation(); //自旋调用 	} } 

    但是这个代码也有自己的问题,我们新加的 if 语句和删除key之间,存在执行时间。有万分之一的可能在期间锁过期,线程B又刚好重新设置了新的key-value,而该代码删除了新的key-value。

    所以,解决这个问题的方法,就是将判断锁与删除锁看成一个事务,事务具有原子性,要做到这种效果,就要使用脚本语言lua。

  2. 具体代码如下:

    void lockKeyMethod(int key) throws InterruptedException {  	String uuid = UUID.randomUUID().toString(); 	String _key = key + ""; 	/** 	 * ## Lua script as below 	 * 	 *   if redis.call('get', KEYS[1]) == ARGV[1] 	 *   then 	 *   	return redis.call('del', KEYS[1]) 	 *   else 	 *   	return 0 	 *   end 	 */ 	String lua_script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";  	Boolean flag = redisTemplate.opsForValue().setIfAbsent(_key, uuid, Duration.ofSeconds(30)); 	if(flag == true){ 		try{ 			//do things 		}catch (Exception e){ 			e.printStackTrace(); 		}finally { 			redisTemplate.execute(new DefaultRedisScript(lua_script, Integer.class), Arrays.asList(_key), uuid); 		} 	}else { 		Thread.sleep(100); 		lockKeyMethod(key); 	} } 
  3. 调用API。
    https://github.com/redisson/redisson
    这是人家官方给的api,也有用法教程,就不多说了。

Springboot+SpringCache+Mybatis

什么是SpringCache?

SpringCache是Spring对一些结果做的缓存,会加速程序的运行。

我们先说怎么配置SpringCache和如何使用它,后面再说SpringCache的一些原理。

配置SpringCache

  1. 添加依赖进pom.xml
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache

    	<dependency> 		<groupId>org.springframework.boot</groupId> 		<artifactId>spring-boot-starter-cache</artifactId> 	</dependency> 
  2. 配置文件指定缓存类型。
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
    主方法添加注解
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache

  3. 连接Mybatis数据库。
    Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache
    (没下载好)

    <!--Kaptcha依赖包--> 	<dependency> 		<groupId>com.baomidou</groupId> 		<artifactId>kaptcha-spring-boot-starter</artifactId> 		<version>1.0.0</version> 	</dependency>  	<dependency> 		<groupId>com.baomidou</groupId> 		<artifactId>mybatis-plus-boot-starter</artifactId> 		<version>3.4.0</version> 	</dependency> 	<dependency> 		<groupId>mysql</groupId> 		<artifactId>mysql-connector-java</artifactId> 		<version>8.0.15</version> 	</dependency> 

    然后就是常规的mysql连接配置操作,略。

Cacheable注解解析

  1. 可以添加在类上,也可以添加在方法上

  2. 在类上就是缓存该类所有的返回值,方法上就是缓存该方法的返回值

  3. key规则有springEL表达式生成,通常用方法参数组合

  4. condition缓存条件,springEL表达式生成,返回true才缓存

  5. value后添加缓存名称,可以有多个

    Cacheable(value={"result"}, key="#root.methodName+'_'+#root.args[0]") 

    springEL表达式,必须用#开头,内部字符串用单引号解释。

    这是干啥的呢?就是当我们查询某一个字段的时候,第一次一定要进入数据库的,但是如果每次都进入数据库,那么数据库压力太大,我们直接用这玩意进行注解。第二次查询同样的字段数据的时候,直接从缓存中提,不会访问数据库。

问题又来了,假设用户更新了数据库的内容,但是再次查找的时候,又不经过数据库,也没人通知缓存有数据更新,怎么办?

我们就将Cacheable替换成CachePut,他专门做数据库更新操作,主要用于数据库数据修改以后对缓存的实时更新。

同理,删除操作我们使用CacheEvict。操作方法同Cacheable。但是这个注解有一个特殊的参数:

  • beforeInvocation = false。意思是数据库中数据被删除后才清除缓存,如果删除异常,缓存不会被删除。
  • beforeInvocation - true。无论删除数据库数据是否出现异常,都删除缓存。

如果需要多注解结合,可以使用@Caching 注解:

@Caching( 	Cacheable = {  		@Cacheable(value = {"result"}, key = "#root.methodName+'_'#para") 	}, 	Put = { 		@CachePut(value = {"update"}, key = "#root.argv[0]") 	} ) public void getName(String para){ 	return name; }