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_第1张图片

Redis.template

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

解决方法:

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

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

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

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

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

  1. 简单的k-v使用StringRedisTemplate替代
  2. 使用Jackson2JsonRedisSerializer。配置直接拷贝即可,不用修改。
    import 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_第9张图片

RedisTemplate连接池

Redis从入门到精通(五):Redis6整合SpringBoot2.x+Mybatis+SpringCache_第10张图片
这里就不说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_第11张图片
    这种问题其实也有解决方法,设置唯一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_第12张图片

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

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

    
    	<dependency>
    		<groupId>com.baomidougroupId>
    		<artifactId>kaptcha-spring-boot-starterartifactId>
    		<version>1.0.0version>
    	dependency>
    
    	<dependency>
    		<groupId>com.baomidougroupId>
    		<artifactId>mybatis-plus-boot-starterartifactId>
    		<version>3.4.0version>
    	dependency>
    	<dependency>
    		<groupId>mysqlgroupId>
    		<artifactId>mysql-connector-javaartifactId>
    		<version>8.0.15version>
    	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;
}

你可能感兴趣的:(Redis,redis)