使用Redis替换MyBatis的二级缓存
Mybatis二级缓存的缺陷
-
服务器重启后,缓存会丢失。
-
Mybatis运行在tomcat容器中,缓存在tomcat容器中。mybatis的二级缓存在集群或者分布式条件下无法共享。
MyBatis的默认缓存是由PerpetualCache
类来实现的,我们可以模仿他,实现一个,以redis作为缓存的自定义缓存类;
第一步:先搭建一个SpringBoot项目
项目结构
第二步:连接数据库,并逆向生成
- 表
逆向生成mapper接口,参考:https://www.cnblogs.com/MLYR/p/14529663.html
第三步:编写Controller
package com.mlyr.controller;
import com.mlyr.mapper.EmpMapper;
import com.mlyr.pojo.Emp;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* @Description:
* @Author: 陌路邑人
* @CreateTime: 2021/3/13
* @Company:
*/
@RestController
@Slf4j
public class EmpController {
@Resource
private EmpMapper empMapper;
//根据id查询
@GetMapping("emp/{empno}")
public Emp findById(@PathVariable Integer empno){
Emp emp = empMapper.selectByPrimaryKey(empno);
log.debug("EMP==="+emp);
return emp;
}
//查询所有
@GetMapping("emp")
public List<Emp> findAll(){
List<Emp> empList = empMapper.selectByExample(null);
log.debug("empList==="+empList);
return empList;
}
//增加
@PostMapping("emp")
public String addEmp(Emp emp){
int f = empMapper.insertSelective(emp);
log.debug("添加==="+f);
return "添加"+f;
}
//修改
@PutMapping("emp")
public String update(Emp emp){
int f = empMapper.updateByPrimaryKeySelective(emp);
log.debug("修改==="+f);
return "修改"+f;
}
//删除
@DeleteMapping("emp/{empno}")
public String deleteByEmpno(@PathVariable Integer empno){
int f = empMapper.deleteByPrimaryKey(empno);
log.debug("删除==="+f);
return "删除"+f;
}
}
第四步:开启Mybatis二级缓存
application.yml
spring:
#配置spring连接mybatis数据库
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?characterEncoding=UTF-8
username: root
password: root
#配置缓存
cache:
type: redis
#配置日志输出
logging:
level:
root: error
com:
mlyr: debug
在EmpMapper.xml中添加
<!--开启二级缓存,指定缓存为自定义的缓存类-->
<cache type="com.mlyr.tools.MyCache" />
第五步:编写自定义织入类
- 就是为了获取Spring中封装的
RedisTemplate
package com.mlyr.autowird;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @Description:
* @Author: 陌路邑人
* @CreateTime: 2021/3/13
* @Company: 自定义织入类,为了获取RedisTemplate
*/
//
@Component
public class MyContextAware implements ApplicationContextAware {
private static ApplicationContext act;
//获取一个Spring容器对象
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
act = applicationContext;
}
//创建一个静态方法,按照name获取容器中的Bean对象
public static Object getBean(String name){
return act.getBean(name);
}
}
第六步:实现自定义缓存类
package com.mlyr.tools;
import com.mlyr.autowird.MyContextAware;
import org.apache.ibatis.cache.Cache;
import org.apache.ibatis.cache.impl.PerpetualCache;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @Description:
* @Author: 陌路邑人
* @CreateTime: 2021/3/13
* @Company:
*/
//作为MyBatis的自定义缓存,不能让Spring容器托管
public class MyCache implements Cache {
//但是又要使用RedisTemplate用来操作redis
private RedisTemplate redisTemplate;
private String id;
private ReentrantReadWriteLock lock;//读写锁,每次读写前要加锁,防止脏读
//只能通过织入类获取RedisTemplate
public MyCache(String id) {
this.id = id;
this.redisTemplate = (RedisTemplate) MyContextAware.getBean("redisTemplate");
lock = new ReentrantReadWriteLock();
}
//缓存标识
@Override
public String getId() {
return id;
}
//放入缓存一次,它会调用一次clear(),清除所有的缓存
// 将数据库中查出来的数据,放入redis数据库中
@Override
public void putObject(Object key, Object value) {
try {
lock.writeLock().lock();
System.out.println("放入缓存===【"+key+"】【"+value+"】");
redisTemplate.opsForHash().put(id,key,value);
int num = 60 + new Random().nextInt(60);
//设置过期时间,只能针对于namespace命名空间进行过期时间
redisTemplate.expire(id,num, TimeUnit.SECONDS);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
//获取缓存中的值,根据key
@Override
public Object getObject(Object key) {
try {
lock.readLock().lock();
System.out.println("获取缓存中的值,根据key==="+key);
return redisTemplate.opsForHash().get(id,key);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
return null;
}
//删除key的缓存
@Override
public Object removeObject(Object key) {
try {
lock.writeLock().lock();
System.out.println("删除key的缓存"+key);
return redisTemplate.opsForHash().delete(id,key);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
return null;
}
//清空id下的所有缓存
@Override
public void clear() {
try {
lock.writeLock().lock();
System.out.println("缓存大小==="+getSize());
redisTemplate.delete(id);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}
//获取缓存的大小
@Override
public int getSize() {
return new Integer(redisTemplate.opsForHash().size(id).toString());
}
}
测试
当第一次访问所有的emp
再次访问
在进行添加一个emp时
再次查询的时候,就会又在数据库中直接查询了
在进行删除、修改的操作时,和上面一样,都会清空缓存,防止脏读
注意点
- 当有多表联查的时候,比如上面的emp与dept,在进行查询所有的emp联查dept后,在进行修改dept数据,缓存中的emp并没有改变,这就产生了脏读!!!
解决多表之间的,另一张表改变,缓存脏读
- 首先在DeptMapper.xml中添加
<cache></cache>
- 然后在EmpMapper.xml中引入dept的namespace
<cache-ref namespace="com.mlyr.mapper.DeptMapper"></cache-ref>
就是相当于两个xml的缓存合在一起,一边的改变能够影响另一边的改变