原创

SpringBoot + MyBatis二级缓存(优化版)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_29777207/article/details/99661769

一、说明

因为业务需要提高系统性能,如意需要设计缓存以及缓存的失效策略。

当前网上流传的二级缓存版本基本千篇一律,并且如果你贸然的用于生产环境的话,name等待你的可能是生产事故。因为很多细节没有进行优化。

之所以选择二级缓存是因为我当前所使用的系统可以拆分成两个部分,一部分mapper对外提供服务,一部分MAPPER仅仅只是作为数据的管理和配置。所以,管理模块仅仅只是对少数人开放,所以可以选择牺牲一部分性能,每次增删改操作时,都清空缓存的MAPPER。

代码大部分来源于网上的demo,不过有些需要注意的地方

1、清空缓存慎用connection.fushDB()方法或者connection.flushAll(),具体原因可以自己谷歌,网上关于这个的答案还是很多的。

2、如何清空是一个设计思路。

3、关于redisTemplate.opsForSet.members()这个方法有一定的问题,有的时候查询的结果会是空的。所以这里使用了scan来代替。

二、代码部分

1、实现mybatis的cache接口

package com.biubiu.cache;

import com.biubiu.util.MD5;
import org.apache.ibatis.cache.Cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class RedisCache implements Cache {
	private static final Logger logger = LoggerFactory.getLogger(RedisCache.class);

	private static RedisTemplate<Object, Object> redisTemplate;

	private final String id;

	//缓存对象的是失效时间,30分钟
	private static final long EXPIRE_TIME_IN_MINUTE = 30;

	//private static final String MYBATIS_CACHE_KEYS_PREFIX = "data-auth-mybatis-";

	private static final String MYBATIS_CACHE_KEYS = "data-auth-mybatis-keys";

	/**
	 * The {@code ReadWriteLock}.
	 */
	private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

	public RedisCache(final String id) {
		if (id == null) {
			throw new IllegalArgumentException("【MAPPER】缓存对象ID不能为空");
		}
		logger.debug("【data-auth-mybatis-id】: " + id);
		this.id = id;
	}

	@Override
	public void clear() {
		//缓存失效策略在CUD时触发,不依赖与自动的
		/*try {
			Set<Object> keys = redisTemplate.opsForSet().members(MYBATIS_CACHE_KEYS);
			if(keys != null && keys.size() != 0) {
				for(Object object : keys) {
					redisTemplate.delete(object);
					redisTemplate.opsForSet().remove(MYBATIS_CACHE_KEYS, object);
				}
				logger.info("清空Mapper缓存");
			}
			//redisTemplate.delete(MYBATIS_CACHE_KEYS);
		} catch (Exception e) {
			logger.error("", e);
		}*/
	}

	public String getId() {
		return this.id;
	}

	public Object getObject(Object key) {
		Object result = null;
		try {
			key = MD5.getMD5Str(key.toString());
			result = redisTemplate.opsForValue().get(key);
			if (result == null) {
				removeObject(key);
				return null;
			}
			logger.info("【MAPPER】【{}】从缓存中获取key【{}】" , this.id, key);
		} catch (Exception e) {
			logger.error("", e);
		}
		return result;
	}

	public ReadWriteLock getReadWriteLock() {
		return this.readWriteLock;
	}

	public int getSize() {
		Long result = 0L;
		try {
			result = redisTemplate.opsForSet().size(MYBATIS_CACHE_KEYS);
			logger.info("【MAPPER】:【{}】的总缓存数为:【{}】" , this.id, (result == null ? 0 : result.intValue()));
		} catch (Exception e) {
			logger.error("", e);
		}
		return result == null ? 0 : result.intValue();
	}

	public void putObject(Object key, Object value) {
		if (value == null || value.toString().equals("[]")) {
			logger.info("【MAPPER】:【{}】的【{}】SQL查询结果为空,不进行缓存" , this.id, key);
			return;
		}
		try {
			key = MD5.getMD5Str(key.toString());
			//将key缓存下来,以便后续删除
			redisTemplate.opsForSet().add(MYBATIS_CACHE_KEYS, key);
			redisTemplate.opsForValue().set(key, value, EXPIRE_TIME_IN_MINUTE, TimeUnit.MINUTES);
			logger.info("【MAPPER】:【{}】添加缓存【{}】" , this.id, key);
		} catch (Exception e) {
			logger.error("", e);
		}
	}

	public Object removeObject(Object key) {
		Object result = null;
		try {
			//清空缓存
			key = MD5.getMD5Str(key.toString());
			result = redisTemplate.delete(key);
			//先删除缓存的数据之后再删除魂村对应的索引
			redisTemplate.opsForSet().remove(MYBATIS_CACHE_KEYS, key);
			logger.info("【MAPPER】:【{}】移除缓存【{}】" , this.id, key);
		} catch (Exception e) {
			logger.error("", e);
		}
		return result;
	}

	public static void setRedisTemplate(RedisTemplate redisTemplate) {
		RedisCache.redisTemplate = redisTemplate;
	}


	public static void clearCache() {
		try {
			Cursor<Object> keys = redisTemplate.opsForSet().scan(MYBATIS_CACHE_KEYS, ScanOptions.NONE);
			while(keys.hasNext()) {
				String key = keys.next().toString();
				redisTemplate.delete(key);
				redisTemplate.opsForSet().remove(MYBATIS_CACHE_KEYS, key);
				logger.info("【MAPPER】: 清除缓存【{}】" , key);
			}
		} catch (Exception e) {
			logger.error("", e);
		}
	}

}
package com.biubiu.cache;

import org.springframework.data.redis.core.RedisTemplate;

public class RedisCacheTransfer {

    public void setRedisTemplate(RedisTemplate redisTemplate) {
        RedisCache.setRedisTemplate(redisTemplate);
    }
}

import导包省略了 

package com.biubiu;


@Configuration
public class Config extends WebMvcConfigurationSupport {


    @Bean(name = "redisTemplate")
    public RedisTemplate<Object, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //设置值(value)的序列化采用FastJsonRedisSerializer。
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
        // 设置键(hashkey)的序列化采用StringRedisSerializer。
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.afterPropertiesSet();
        //开启事物
        //redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }

    @Bean
    public RedisCacheTransfer redisCacheTransfer(LettuceConnectionFactory lettuceConnectionFactory) {
        RedisCacheTransfer redisCacheTransfer = new RedisCacheTransfer();
        redisCacheTransfer.setRedisTemplate(redisTemplate(lettuceConnectionFactory));
        return redisCacheTransfer;
    }


}

 

2、mybatis的xml文件里面进行配置

<cache type="com.biubiu.cache.RedisCache"/>

 

3、springboot的yml文件中的配置

spring:
  redis:
    host: 127.0.0.1
    database: 13
    password: 123456
    lettuce:
      pool:
        max-wait: 10000ms
        max-idle: 10
        max-active: 100
    timeout: 50000ms
  cache:
    type: redis
    redis:
      time-to-live: 1h

mybatis:
  configuration:
    cache-enabled: true
    lazy-loading-enabled: true
    use-column-label: true
    use-generated-keys: true
    map-underscore-to-camel-case: true
    multiple-result-sets-enabled: true
    auto-mapping-behavior: partial
    safe-row-bounds-enabled: false
    local-cache-scope: session
    jdbc-type-for-null: other
    lazy-load-trigger-methods: equals,clone,hashCode,toString
    aggressive-lazy-loading: true
  mapper-locations: classpath:com/biubiu/mapper/impl/*.xml

 

 

4、pom文件中关于打包xml文件的配置,可以根据你自己的习惯来设置

<build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
        </resources>
    </build>

5、如何清空

package com.biubiu.annotation;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheRemove {
}

 

package com.biubiu.aop;

import com.biubiu.cache.RedisCache;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class CacheRemoveAspect {

    @Around("@annotation(com.biubiu.annotation.CacheRemove)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        RedisCache.clearCache();
        Object val = joinPoint.proceed();
        RedisCache.clearCache();
        return val;
    }

}

6、在你需要清空的操作上加上注解就OK了

 

7、工具类MD5

package com.biubiu.util;

import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;


/**
 * @Author Kulen
 * @CreateTime 2010-6-16下午05:28:11
 * @Version 1.0
 * @Explanation 用MD5对数据进行加密
 */
public class MD5 {

	static final Logger log = LogManager.getLogger(MD5.class);

	MessageDigest md5;

	static final char hexDigits[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

	public MD5() {
		try {
			// 获得MD5摘要算法的 MessageDigest 对象
			md5 = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			log.error("创建MD5对象出错, ", e);
			throw new IllegalArgumentException("创建md5对象时出错");
		}
	}

	public synchronized String getMD5(String s) {
		return this.getMD5(s.getBytes()).toLowerCase();
	}

	public synchronized String getMD5(byte[] btInput) {
		try {
			// 使用指定的字节更新摘要
			md5.update(btInput);
			// 获得密文
			byte[] md = md5.digest();
			// 把密文转换成十六进制的字符串形式
			int j = md.length;
			char str[] = new char[j * 2];
			int k = 0;
			for (int i = 0; i < j; i++) {
				byte byte0 = md[i];
				str[k++] = hexDigits[byte0 >>> 4 & 0xf];
				str[k++] = hexDigits[byte0 & 0xf];
			}
			return new String(str).toLowerCase();
		} catch (Exception e) {
			log.error("生成MD5码时出错,", e);
			throw new IllegalArgumentException("生成MD5出错");
		}
	}
	
	 /**
	  * 获取32位的MD5加密
	  * @param sourceStr
	  * @return
	  */
	public static String getMD5Str(String sourceStr) {
	        String result = "";
	        try {
	            MessageDigest md = MessageDigest.getInstance("MD5");
	            md.update(sourceStr.getBytes(Charset.forName("utf-8")));
	            byte b[] = md.digest();
	            int i;
	            StringBuffer buf = new StringBuffer("");
	            for (int offset = 0; offset < b.length; offset++) {
	                i = b[offset];
	                if (i < 0)
	                    i += 256;
	                if (i < 16)
	                    buf.append("0");
	                buf.append(Integer.toHexString(i));
	            }
	            result = buf.toString();
	        } catch (NoSuchAlgorithmException e) {
	            System.out.println(e);
	        }
	        return result;
   }

}

 

推荐一个公众号

号主为一线大厂架构师,CSDN博客专家,博客访问量突破一千万。主要分享Java、golang架构,源码,分布式,高并发等技术,用大厂程序员的视角来探讨技术进阶、面试指南、职业规划等。15W技术人的选择!

 

 

 

 

文章最后发布于: 2019-08-15 23:36:17
展开阅读全文
0 个人打赏

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 1024 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览