redis实现分布式缓存
🔥在网上看到很多利用redis实现缓存的贴子,但是大多数都是用到AOP实现的,于是就想写一篇关于mybatis二级缓存结合redis实现redis分布式缓存,mybatis二级缓存只能作用于xml中写的sql语句
🔥环境搭建
📌目录结构
一如既往的springboot标准结构,直接通过idea创建即可
📌pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
📌application.properties
#server
server.port=8080
#jdbc
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/redis_test?useSSL=false&useUnicode=true&characterEncoding=utf8
spring.datasource.password=123456
spring.datasource.username=root
#mybatis-plus
mybatis-plus.mapper-locations=classpath:mapper/*.xml
#控制台打印sql
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
📌SpringContextUtil
@Component
@Lazy(false)
public class SpringContextUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext; // Spring应用上下文环境
@Override
public void setApplicationContext(ApplicationContext arg0) throws BeansException {
SpringContextUtil.applicationContext = arg0;
}
public static ApplicationContext getApplicationContext(String redisTemplate) {
return applicationContext;
}
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) throws BeansException {
return (T) applicationContext.getBean(name);
}
}
📌测试类
@SpringBootTest
class RedisTestApplicationTests {
@Autowired
private UserServie userServie;
@Autowired
private DeptServie deptServie;
@Autowired
private RedisTemplate redisTemplate;
@Test
void findAll() {
userServie.findAll();
userServie.findAll();
}
@Test
void addUser(){
User user = new User();
user.setUName("xiaoxin");
user.setAge(19);
userServie.addUser(user);
}
@Test
void getUserAndDeptById(){
System.out.println(userServie.getUserAndDeptById(1));
}
@Test
void findDeptAll(){
deptServie.findDeptAll().forEach((depts)->System.out.println(depts));
deptServie.findDeptAll().forEach((depts)->System.out.println(depts));
}
@Test
void addDept(){
Dept dept = new Dept();
dept.setDid(1);
dept.setDeptName("开发部");
deptServie.addDept(dept);
}
@Test
void delDeptById(){
deptServie.delDeptById(1);
}
}
🔥mybatis二级缓存
mybatis使用二级缓存非常的简单,只需要在需要添加缓存的xml文件加入如下标签,就能够实现对user表的缓存,但是mybatis二级缓存是与项目在一同一台机器内存中,会对项目的性能有很大影响,也不支持久化等等,不推荐这种方式
🔥利用mybatis二级缓存&redis实现分布式缓存
这种方式下项目和缓存没有部署在同一台机器上,而且可拓展性也更强,首先我们看一下myabtis实现缓存的Cache接口的结构,其实它就是通过PerpetualCache类去配置实现缓存数据的
我们通过debug,执行测试类中的findAll()方法可以看到确实进入这个PerpetualCache类,问题就变得很明了!我们只需要自己定义一个类去实现Cache接口,修改默认的缓存配置类,将覆盖PerpetualCache类中存储缓存的方式换成redis方式存储即可
开启mybatis二级缓存
<!-- 开启mybatis二级缓存-->
<cache type="com.dasuan.demo.config.RedisCache"/>
实现Cache接口,重写里面的方法。目前没有进行任何配置
public class RedisCache implements Cache {
@Override
public String getId() {
return null;
}
@Override
public void putObject(Object o, Object o1) {
}
@Override
public Object getObject(Object o) {
return null;
}
@Override
public Object removeObject(Object o) {
return null;
}
@Override
public void clear() {
}
@Override
public int getSize() {
return 0;
}
}
完整的配置,下面我会详细的说下为什么要这样配置!!
public class RedisCache implements Cache {
private final String id;
//必须存在的构造方法
public RedisCache(String id){
System.out.println("id:============================================>"+id);
this.id = id;
}
@Override
public String getId() {
return this.id;
}
@Override
public void putObject(Object key, Object value) {
System.out.println("key:============================================>"+key);
System.out.println("value:==========================================>"+value);
//利用redis中Hash类型存储
getRedisTemplate().opsForHash().put(this.id,key,value);
}
@Override
public Object getObject(Object key) {
System.out.println("key:============================================>"+key);
//从redis中获取对应的value
return getRedisTemplate().opsForHash().get(this.id,key);
}
@Override
public Object removeObject(Object key) {
System.out.println("removeObject:方法的key========================================>"+key);
return null;
}
//claer()方法在更新操作的时候会执行情况缓存
@Override
public void clear() {
System.out.println("进入了clear方法()=============================>");
//从redis中删除对应的key
getRedisTemplate().delete(this.id);
}
//用来计算缓存的数据流 可以自己测试一下
@Override
public int getSize() {
return 0;
}
public RedisTemplate getRedisTemplate(){
return SpringContextUtil.getBean("redisTemplate");
}
}
🔥这是mybatis源码中PerpetualCache类的配置,它默认使用是mybatis中的缓存,我们可以模仿着源码进行配置,将默认缓存改成以redis缓存实现
public class PerpetualCache implements Cache {
private final String id;
private final Map<Object, Object> cache = new HashMap();
public PerpetualCache(String id) {
this.id = id;
}
public String getId() {
return this.id;
}
public int getSize() {
return this.cache.size();
}
public void putObject(Object key, Object value) {
this.cache.put(key, value);
}
public Object getObject(Object key) {
return this.cache.get(key);
}
public Object removeObject(Object key) {
return this.cache.remove(key);
}
public void clear() {
this.cache.clear();
}
public boolean equals(Object o) {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else if (this == o) {
return true;
} else if (!(o instanceof Cache)) {
return false;
} else {
Cache otherCache = (Cache)o;
return this.getId().equals(otherCache.getId());
}
}
public int hashCode() {
if (this.getId() == null) {
throw new CacheException("Cache instances require an ID.");
} else {
return this.getId().hashCode();
}
}
}
下面我将要介绍为什么要这样配置!!
首先直接自定义实现Cache接口,重写里面的方法。不进行任何配置,然后开启mybatis二级缓存
我们直接执行测试类中findAll()方法,在报错信息中我们可以看到如下一段描述,报错的大致意思是基本缓存实现必须有一个以 String id 作为参数的构造函数。这个时候我们可能不明白它这个报错到底是啥意思,不要急!我们可以看下mybatis源码中PerpetualCache类
通过报错和对比mybatis源码中PerpetualCache类,大概可以确定我们缺少的是如下两行代码
在自定义的Cache类加上如下代码,并设置getId方法的返回值,执行测试类中findAll方法
我们可以看到id打印出来的是一个类路径名,先不管,我们继续看下面的putObject(Object key, Object value),这个方法非常重要,通过方法名咱们已经可以看出这是一个放入缓存数据库的方法
在自定义的Cache类加上如下代码,先来看一下key和value到底是个什么?执行测试类中findAll()方法
key是一段各种信息的组合,可以作为唯一标识,value是一个对象的集合
这个时候我们一共获得了三个值 id , key , value 并且哪怕数据库数据发生了改变,只有value会跟着变化,其余两个都不会。这时候我马上可以想到redis中Hash类型,hash结构为一个大key ,value又是一个键值对,根据一个大key,和键值对里面的key,唯一确定一个value
通过redis中Hash类型存储缓存数据,执行测试类中findAll()方法
可以看到redis中已经存入了这条数据,但是每一次查询仍然是从数据库获取的数据,这是为什么呢?别急!我们继续看下一个重要的方法getObject(Object key)
和上面一样继续getObject(Object key)打印key,先来看一下key到底是个什么?执行测试类中findAll()方法
从以下截图中,我们可以看出两个信息,一个是每次getObject(Object key),是在putObject(Object key, Object value)之前执行的,也就是在我们在执行查询方法的时候,会先查询缓存中是否有数据,如果没有数据就将查询到的数据存入缓存中,另外一个信息是这个两个方法中的key是一模一样的,并且细心的朋友会发现id在每一次执行方法的时候都会打印全类路径名
通过一个大key,和键值对里面的key,唯一确定一个value
执行测试类中findAll()方法,为了方便查看效果,我清除了redis中的数据
执行测试类中findAll()方法,查询到的数据又存储到了redis中
如何更新缓存数据库呢?很简单!只要进行的更新操作,会进入下面的clear()方法执行清除缓存方法 注意:并不会进入removeObject(),这是一个无效的方法
执行测试类中delDeptById()方法,可以看到进入了clear()方法
注意redis中数据库变化,可以看到删除一条记录后,DeptDao中缓存全部被清除了,添加和修改同样也是这样,这里就不做演示了
至此为止,通过查询数据,如果缓存里面没有数据,就会从数据库中查询数据,然后放到缓存,通过更新数据库可以清空相应的Dao中的缓存,缓存看起来基本所有都配置好了,但实际还存在一个问题,看下来的例子
执行getUserAndDeptById()方法,这是一个关联查询,查询的是用户信息及其对应的部门信息,可以看到上一步操作已经将1号部门给删了,数据库中也不存在1号部门信息了,而现在依然可以通过缓存查询到了已经删除掉的部门信息
为什么出现这样的问题呢,因为每个Dao中的缓存是没有任何关联的getUserAndDeptById()的缓存数据是存储到UserDao中的,虽然数据库中已经删掉部门信息,但是缓存中仍然存在存储在UserDao中getUserAndDeptById()方法的缓存数据
上述现象肯定是不行的,要么不删,要么把相关联的缓存数据都得删掉才合理,这是需要使用到如下标签来关联缓存
这个标签可以在任意一个Dao中添加,如在DeptDao中添加,关联缓存标签中namesoace值为与其关联的UserDao全类路径名,反之也是如此,关联后DeptDao和UserDao中的所有缓存数据都存储在UserDao中,全部由UserDao管理