Redis + Mybatis实现分布式缓存
0、预备知识
为什么需要缓存?
为了高性能、高并发。就这么简单😂😂😂
什么是缓存(Cache)
缓存就是数据交换的缓冲区(称作Cache),是存贮数据(使用频繁的数据)的临时地方。当用户查询数据,首先在缓存中寻找,如果找到了则直接执行。如果找不到,则去数据库中查找。
缓存的本质就是用空间换时间,牺牲数据的实时性,以服务器内存中的数据暂时代替从数据库读取最新的数据,减少数据库IO,减轻服务器压力,减少网络延迟,加快页面打开速度。
缓存是⼀个高速数据交换的存储器,使用它可以快速的访问和操作数据。将热点资源(高频读、低频写)提前放入离用户最近、访问速度更快的地方,以提高访问速度,这就是缓存的本质。
什么是数据库缓存?
缓存有多种多样,比如文件缓存、浏览器缓存、数据库缓存、Web应用层缓存、服务器缓存、CDN缓存等等,而我们今天这里聊的是数据库缓存。
数据库缓存:常用的缓存方案有memcached、redis、mongoDB等。把经常需要从数据库查询的数据、或经常更新的数据放入到缓存中,这样下次查询时,直接从缓存直接返回,减轻数据库压力,提升数据库性能。
缓存VS数据库
-
缓存的数据是存储在
内存
中的,而数据库的数据是存储在磁盘
中的,因为内存的操作性能远远大于磁盘,因此缓存的查询效率会高很多; -
缓存一般都是通过 key-value 查询数据,因为不像数据库那样还有查询的条件等因素,所以查询的性能⼀般会比数据库高;
-
缓存更容易做分布式部署(当⼀台服务器变成多台相连的服务器集群),而数据库⼀般比较难实现分布式部署,因此缓存的负载和性能更容易平行扩展和增加。
什么是分布式缓存?
根据缓存是否与应用进程属于同一进程(单机与多机),可分为本地缓存和分布式缓存。
-
本地缓存也叫做单机缓存,即将服务部署到一台服务器上,所以本地缓存只适用于当前系统。或者说存在应用服务器内存中数据称之为本地缓存(local cache)。
-
分布式缓存也叫做多机缓存,即将服务部署到多台服务器上,并且通过负载分发将用户的请求按照一定的规则分发到不同服务器。也就是存储在当前应用服务器内存之外数据称之为分布式缓存(distribute cache)。
扩展
集群:就是一组计算机,它们作为一个整体向用户提供一组网络资源,这些单个的计算机系统就是集群的节点。也就是将同一种服务的多个节点放在一起共同对系统提供服务过程称之为集群。
分布式:由多个不同服务器集群功能对系统提供服务,这个系统称之为分布式系统(distribute system)。
1、Mybatis 实现本地缓存
我们知道,在我们经常使用的持久层框架Mybatis中,可以轻松使用它提供的缓存机制,主要是有两级缓存。
-
提前准备好一个数据库和基础表。
-
使用Spring Initializr快速初始化一个基础项目,注意勾选Lombok、mybatis、MySQL这几个技术所用到的依赖。
具体看这里手把手演示创建项目:
-
然后在配置文件中配置数据源以及打印Mybatis的SQL执行日志。
-
MyBatis的一级缓存
-
一级缓存是
SqlSession
级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问。 -
一级缓存是默认开启的。
示例:
/**
* @Classname UserMapper
* @Description TODO
* @Date 2022/8/23 20:56
* @Created by YJS
* @WebSite www.imyjs.cn
*/
@Mapper
public interface UserMapper {
@Select(" SELECT * FROM sc2")
List<User> getAllUsers();
}
@Autowired
SqlSessionFactory factory;
@Test
public void testOneCache(){
SqlSession sqlSession = factory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
// 第一次执行selectUserById(1);
System.out.println(mapper.getUserById(1));
// 第二次执行selectUserById(1);
System.out.println(mapper.getUserById(1));
}
SqlSession
,需要去Spring 容器中获取一个SqlSessionFactory
// 第一次执行selectUserById(1); 未命中缓存需要查询数据库
JDBC Connection [HikariProxyConnection@2144912729 wrapping com.mysql.cj.jdbc.ConnectionImpl@44c13103] will not be managed by Spring
==> Preparing: SELECT * FROM sc2 WHERE sid=?
==> Parameters: 1(Integer)
<== Columns: sid, sname, score
<== Row: 1, 小白, 100
<== Total: 1
User(sid=1, name=小白, score=100)
// 第二次执行selectUserById(1); 不再执行SQL语句!直接从缓存中取!
User(sid=1, name=小白, score=100)
-
不同的SqlSession对应不同的一级缓存
-
同一个SqlSession但是查询条件不同
-
同一个SqlSession两次查询期间执行了任何一次增删改操作
-
sqlSession.clearCache();
-
二级缓存是
SqlSessionFactory
级别,通过同一个SqlSessionFactory
创建的SqlSession查询的结果会被缓存;此后若再次执行相同的查询语句,结果就会从缓存中获取。 -
二级缓存开启的条件
-
在核心配置文件中,设置全局配置属性cacheEnabled="true",默认为true,不需要设置
-
在映射文件中设置标签
<cache />
。注意:在使用注解书写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="cn.imyjs.mapper.UserMapper"> <cache /> </mapper>
- 二级缓存必须在SqlSession关闭或提交之后有效:
sqlSession.close();
- 查询的数据所转换的实体类类型必须实现序列化的接口,否则报错!:
public class User implements Serializable {}
-
-
@Test public void testTwoCache(){ SqlSession sqlSession = factory.openSession(); UserMapper mapper = sqlSession.getMapper(UserMapper.class); System.out.println(mapper.getUserById(2)); sqlSession.close(); SqlSession sqlSession2 = factory.openSession(); UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class); System.out.println(mapper2.getUserById(2)); sqlSession2.close(); } // 打印结果: // 第一次执行selectUserById(2); 未命中缓存需要查询数据库 JDBC Connection [HikariProxyConnection@485237151 wrapping com.mysql.cj.jdbc.ConnectionImpl@692e028d] will not be managed by Spring ==> Preparing: SELECT * FROM sc2 WHERE sid=? ==> Parameters: 2(Integer) <== Columns: sid, sname, score <== Row: 2, 小红, 64 <== Total: 1 User(sid=2, name=小红, score=64) // 第二次执行selectUserById(2); 不再执行SQL语句!直接从缓存中取! Cache Hit Ratio [cn.imyjs.mapper.UserMapper]: 0.5 // 缓存命中率 User(sid=2, name=小红, score=64)
SqlSessionFactory
创建的SqlSession查询的结果会被缓存:
@Test
public void testTwoCache(){
System.out.println(userMapper.getUserById(2));
System.out.println(userMapper.getUserById(2));
}
<cache />
,另外需要在mapper接口上添加注解:二级缓存的相关配置
-
在mapper配置文件中添加的cache标签可以设置一些属性
-
eviction
属性:缓存回收策略-
LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象。
-
FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们。
-
SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象。
-
WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
-
默认的是 LRU
-
-
flushInterval
属性:刷新间隔,单位毫秒-
默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用语句(增删改)时刷新
-
-
size
属性:引用数目,正整数-
代表缓存最多可以存储多少个对象,太大容易导致内存溢出
-
-
readOnly
属性:只读,true/false-
true:只读缓存;会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改。这提供了很重要的性能优势。
-
false:读写缓存;会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false
-
-
type
属性:指定自定义缓存的全类名(实现Cache接口即可)
MyBatis缓存查询的顺序
-
先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
-
如果二级缓存没有命中,再查询一级缓存
-
如果一级缓存也没有命中,则查询数据库
-
SqlSession关闭之后,一级缓存中的数据会写入二级缓存
2、Redis 实现分布式缓存
Mybatis底层默认是通过org.apache.ibatis.cache.impl.PerpetualCache
实现的二级缓存。通过PerpetualCache
默认源码的知 可以使用自定义Cache类 implements Cache接口,并对里面方法进行实现。然后可以修改cache标签
的type属性
来自定义缓存。那么我们下边就是自定义一个RedisCache实现类。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# 配置数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/db1?characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
spring.datasource.username=root
spring.datasource.password=123
# 控制台显示日志
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# redis
spring.redis.database=0
spring.redis.port=6379
spring.redis.host=192.168.149.142
在自定义RedisCache类实现Cache接口之前,我们首先需要去创建一个ApplicationContextUtils
工具类,因为需要获取redisTemplate来操作Redis,这个类是由Spring工厂管理,那么我们就需要通过注入RedisTemplate来获取redisTemplate这个对象,但是我们通过实现Cache类实现自定义缓存的类不是由Spring工厂管理的,是由Mybatis实例化的,通过上面所说的方法注入RedisTemplate来获取redisTemplate这个对象。所以,我们可以去工厂启动的时候去拿,这就需要拿到springboot创建好的工厂,再去获取redisTemplate对象。
/**
* @Classname ApplicationContextUtils
* @Description TODO
* @Date 2022/8/23 22:03
* @Created by YJS
* @WebSite www.imyjs.cn
*/
@Component
public class ApplicationContextUtils implements ApplicationContextAware {
//保存下来工厂
private static ApplicationContext applicationContext;
//将创建好的工厂以参数的形式传递给这个类
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
//提供在工厂中获取对象的方法
public static Object getBean(String beanName){
return applicationContext.getBean(beanName);
}
}
下面就是创建自定义RedisCache类,来重写里面的一些方法,重点需要关注三个:putObject()、getObject()、clear().
在实现自定义RedisCache时我们必须满足以下几点要求:
-
实现一个String类型的id的构造方法
-
返回唯一标识id不能为空
-
除了 removeObject 方法以外,其他方法都要做相应的实现
package cn.imyjs.cache;
import cn.imyjs.utils.ApplicationContextUtils;
import org.apache.ibatis.cache.Cache;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.DigestUtils;
/**
* @Classname RedisCache
* @Description 自定义Redis缓存
* @Date 2022/8/23 22:01
* @Created by YJS
* @WebSite www.imyjs.cn
*/
@SuppressWarnings("all")
public class RedisCache implements Cache {
//当前放入缓存的mapper的namespace
private final String id;
//必须存在构造方法
public RedisCache(String id){
this.id = id;
System.out.println("大KEY:================>" + id);
}
@Override
public String getId() {
return this.id;
}
//缓存放值
@Override
public void putObject(Object key, Object value) {
// System.out.println("存放的key值="+getKeyMd5(key.toString()));
// System.out.println("存放的value值="+value.toString());
//使用redis中的hash类型作为缓存存储模型 KEY key value
System.out.println("id=" + id);
System.out.println("key=" + getKeyMd5(key.toString()));
System.out.println("value=" + value);
getRedisTemplate().opsForHash().put(id, getKeyMd5(key.toString()), value);
}
//缓存取值
@Override
public Object getObject(Object key) {
System.out.println("获取的key值="+getKeyMd5(key.toString()));
//根据key从redis获取hash类型中获取数据
return getRedisTemplate().opsForHash().get(id, getKeyMd5(key.toString()));
}
//根据指定key删除缓存
//此方法为mybatis的保留方法,默认无实现
@Override
public Object removeObject(Object o) {
System.out.println("根据指定key删除缓存");
return null;
}
//清空缓存
@Override
public void clear() {
System.out.println("清空缓存");
getRedisTemplate().delete(id);
}
@Override
public int getSize() {
//获取list中 key value数
return getRedisTemplate().opsForList().size(id).intValue();
}
//封装redistemplate
public RedisTemplate getRedisTemplate(){
//通过RedisUtils工具类获取redisTemplate
RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
redisTemplate.setKeySerializer(new StringRedisSerializer());
// redisTemplate.setHashValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
// 优化KEY过长问题,使用MD5加密
private String getKeyMd5(String key){
return DigestUtils.md5DigestAsHex(key.getBytes());
}
}
修改mapper.xml
最后就是在mapper.xml文件中,将<cache/>
改为 <cache type="cn.imyjs.cache.RedisCache" />
,实现我们自定义的缓存。
如果是注解方式开发,只需要在Mapper接口类上添加以下信息:
@Mapper
@CacheNamespace(blocking = true, implementation = RedisCache.class)
public interface UserMapper {
@Select(" SELECT * FROM sc2")
List<User> getAllUsers();
@Select("SELECT * FROM sc2 WHERE sid=#{sid}")
User getUserById(Integer sid);
}
到此为止,我们的分布式缓存已经实现了。
经过一段测试,可以得出以下结论:
-
-
增删改默认会清空redis缓存(flushCache=true)。
当然flushCache
,useCache
可以控制是否将数据放入缓存以及清空缓存
(1)当为select语句时:
flushCache默认为false,表示任何时候语句被调用,都不会去清空本地缓存和二级缓存。
useCache默认为true,表示会将本条语句的结果进行二级缓存。
(2)当为insert、update、delete语句时:
flushCache默认为true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空。
useCache属性在该情况下没有。
优化缓存
通过观察发现,有的KEY有时会很长,那么此时就会消耗一定的性能,所以,我们可以使用MD5加密算法进行一个小小的优化,经过MD5加密后的KEY返回得到的是一个长度固定为32位的字符串,并且可以保证相同的KEY得到的字符串一定是相同的,而且MD5是不可逆的,关于MD5加密算法在这里不再赘述。
// 优化KEY过长问题,使用MD5加密
private String getKeyMd5(String key){
return DigestUtils.md5DigestAsHex(key.getBytes());
}
如果项目中表查询之间没有任何关联查询使用现在的这种缓存方式没有任何问题,现有缓存方式在表连接查询过程中一定存在问题。
此时可以使用<cache-ref/>
标签来解决。
<!--使用自定义的RedisCache,如果Mapper之间的操作表之间无关系使用这个-->
<!--<cache type="cn.imyjs.cache.RedisCache"/>-->
<!--关联关系缓存处理时使用这个--> <!--引用另一方的缓存-->
<cache-ref namespace="cn.imyjs.mapper.UserMapper"/>
存入Redis中的数据是Hash类型,大Key是mapper配置文件中namespace中的名字,value中的key是大Key和查询语句等组合成的字符串,不过我们使用了md5技术,所以value中的key是一个32位的16进制的字符串,并且在进行增、删、改操作的时候会清空缓存。