Bootstrap

mybatis+redis使用

目录

一,mybatis缓存机制  mybatis提供了一级、二级缓存。

二,不使用缓存情况

三,开启二级缓存

四、分布式缓存

1.自定义redis  cache

2.使用自定义的RedisCache

3.完善RedisCache

4.测试

五,出现的问题

六,优化

六,面试题

缓存穿透

缓存雪崩

缓存击穿


一,mybatis缓存机制  mybatis提供了一级、二级缓存。

一级缓存:线程级别的缓存,也称为本地缓存或sqlSession级别的缓存,一级缓存是默认存在的,同一个会话中,查询两次相同的操作就会从缓存中取。
二级缓存:全局范围的缓存;除了当前sqlSession能用外,其他的也可以使用。二级缓存默认也是开启的,只需要在mapper文件中写一个<cache/>即可实现,二级缓存的实现需要pojo实现序列化的接口,否则会出错

二,不使用缓存情况


首先我们先试试没有开启缓存的情况:

查询sql的代码如下,我这里使用的service会调用mapper实现一个分页查询:

List<UserListVO> s1 = service.getUserListVOByPage(2, 5);
List<UserListVO> s2 = service.getUserListVOByPage(2, 5);
List<UserListVO> s3 = service.getUserListVOByPage(1, 3);
大致结果如下:

==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
 
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
 
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<==        Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<==        Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<==      Total: 3

可以看到,执行了3次jdbc的查询操作,虽然前两次查询都是一模一样的,但是还是查询了多次数据库,这时候你可能会好奇不是有一级缓存吗?

因为我们这里使用的是service,而每一次service的调用都会重新创建一个新的数据库会话,当service方法调用结束后就会自动的提交事务、关闭会话,所有这种情况一级缓存是管不到的,只能使用二级缓存来减轻数据库的压力。

三,开启二级缓存

具体步骤:

1.最好在mybatis配置文件中显示的开启二级缓存

2.在mapper文件中加入使用二级缓存的标志 

3.让要查询的pojo类实现序列化接口

这时候再来查询,结果如下:

Creating a new SqlSession
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.0
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@32fd5bc] will be managed by Spring
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
Releasing transactional SqlSession 
Creating a new SqlSession
Registering transaction synchronization for SqlSession 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.5
Releasing transactional SqlSession 
Creating a new SqlSession
 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.3333333333333333
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@32fd5bc] will be managed by Spring
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<==        Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<==        Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<==      Total: 3

可以看到前两次相同的查询只查询了一次,第二次查询是在二级缓存中直接拿到的数据。简单的本地缓存也就实现了。

接下来是分布式缓存的实现

四、分布式缓存


上面提到的,实现二级缓存,需要在mapper中写上<cache/>标签,该标签实际对应着mybatis提供的Cache接口,该接口有多个实现类用于提供二级缓存的实现:

 其中默认的使用PerpetualCache这个类:

 它的实现就是将sql语句作为key,数据作为value存储在一个哈希表中,这是默认的情况,现在我们想要实现redis缓存,且这些缓存数据不放在服务器应用上,所有我们需要自己写一个实现了Cache接口的缓存类,把该缓存的内容存储到我们的redis服务器,拿缓存也是从redis服务器拿。

1.自定义redis  cache


首先我们新建一个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;
    }
}


 实现了Cache的类使用时必须给一个构造方法,带一个String类型的id,可以参考上面的PerpetualCache

getId():返回cache的唯一id,构造方法传过来的
putObject(): 将数据放入缓存
getObject():从缓存中取出数据
removeObject():移除一个缓存
clear():清空所有缓存
getSize():取得缓存的个数


2.使用自定义的RedisCache


将cache标签的默认值改为我们新建的类

 日后,该mapper中的二级缓存都会在redis中存或者取。

3.完善RedisCache


初始化的代码如下:

该类的对象必须是由mybatis创建的,mybatis会给每个mapper文件创建一个对象,每个对象的id都不一样。

private final String id;
 
public RedisCache(String id) {
    this.id = id;
}
 
@Override
public String getId() {
    return id;
}


到这一步,我们的RedisCache就能够启动了,但是并不能存取缓存数据,需要实现剩下的方法,但是有个问题,就是我们装缓存的容器需要时redis,所有需要操作Redis,我们可以使用上一篇讲到的redis整合springboot。但是RedisCache并不是IOC容器中的,不能够直接注入RedisTemplate,我们先自定义个一个获取IOC容器的工具类,用它来获取IOC容器中的redisTemplate:

@Component
//需要继承ApplicationContextAware
public class ApplicationContextUtils implements ApplicationContextAware {
    //获取到ioc容器
    private static ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
    //获取ioc容器
    public static ApplicationContext getContext(){
        return context;
    }
    //直接获取bean对象
    public static Object getBean(String bean){
        return context.getBean(bean);
    }
}

现在我们就可以在RedisCache中获取IOC容器里面的对象了:

再实现所有方法,代码如下:

public class RedisCache implements Cache {
    private final String id;
 
    public RedisCache(String id) {
        this.id = id;
    }
 
    @Override
    public String getId() {
        return id;
    }
 
    @Override
    public void putObject(Object o, Object o1) {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        redis.opsForHash().put(id, o, o1);
    }
 
    @Override
    public Object getObject(Object o) {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        return redis.opsForHash().get(id, o);
    }
 
    @Override
    public Object removeObject(Object o) {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        return redis.opsForHash().delete(id, o);
    }
 
    @Override
    public void clear() {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        redis.delete(id);
    }
 
    @Override
    public int getSize() {
        RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");
        return redis.opsForHash().size(id).intValue();
    }
}


把mybatis创建时提供的id作为我们redis中的key,创建一个hashmap的数据结构,hashmap的key就是查询的sql语句,value为对应的数据。

再说明一点,代码中的objectRedisTemplate是我自定义的redisTemplate,为了方便,把它的序列化方式全部改为了jackson的形式 

4.测试


在启动我们的代码测试:

 结果如下:

Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.0
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<==        Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<==        Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<==        Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<==        Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<==      Total: 5
 
 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.5
 
 
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.3333333333333333
==>  Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<==    Columns: id, username, password, name, gender, role, balance, telephone
<==        Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<==        Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<==        Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<==      Total: 3


可以看到,使用到了二级缓存,重复的查找只查找了一次,第二次是在缓存中获取到的,redis中的数据如下:

正好两个对应有两个键值对,没有任何问题。这两个查询的值已经到我们的redis服务器中了,如果再启动一次测试代码,那么更简单,直接全部在redis服务器中取,一次数据库的查询都不需要。

五,出现的问题


再者,当我们执行一个增删改操作时

 就会调用clear方法,清空redis中该key对应的所有数据,也就是以mapper文件为单位进行情况,根据id为标志。并不能影响到其他的表。

如果多张表不存在任何的关联查询,就不会出现问题,但如果其中有关联查询,如果修改了其中一个表,紧接着删除了该表的所有缓存,但另一张关联的表是不会情况缓存的,这时候就会出现查询到的数据与数据库不统一的情况。

这时候就需要修改一些,使得我们有关联关系的两张表增删改其中的一张表就清空所有的关联表的缓存,不仅仅删除自身的缓存。所有我们就不能在每张表都设置一个cache标签了,让两个有关联关系的表共用一个RedisCache对象,如下:

 只需要一个cache标签即可,这样的话这两个mapper的缓存就在一起了,清空也是全清空。

六,优化


这一步主要是对缓存的键值对的一个优化措施,从上面的图片可以看到,key对应于一条sql语句加上其他的一些信息,看起来很冗长,这样会影响redis的性能,我们要尽可能设计的简洁一下。

我们的目的是让key变短,且必须是唯一的,不能够冲突,这一特点可以使用加密算法那一模板的报文摘要技术,把一个长的数据变为一个固定长度的数据,且能够唯一的区分。

最常用的报文摘要就是即MD5,我们就将key进行MD5加密,再存放到redis服务器中。

写为这种形式:

 重新清空redis,并运行查询后结果如下:

 键名的长度得到了明显的缩减,查找速度也会变快一点。

相应的也能给缓存设置一个超时时间。

六,面试题


缓存穿透


缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在(如查找id为-1的数据),这样缓存永远不会生效,这些恶意请求都会到达数据库,让数据库承受巨大的压力。

有常用的两种解决方法:

缓存空对象:把查到为空的数据也缓存在缓存中,下次再有相同的形式查找就会从缓存中拿到空数据。
优点:实现简单,维护方便
缺点:1.额外的内存消耗(可能会恶意使用随机值查找),解决方法为设置超时时间;2.可能造成短期的不一致
布隆过滤:请求时先访问布隆过滤器,如果存在要查的数据就放行,如果不存在就拒绝。
优点:内存占用小,没有多余key 
缺点:可能误判;实现复杂
实时监控:发现Redis的命中率变低了,就记性排查


另外还可以通过增强id的复杂度,避免被操作id的规律、做好基础校验等主动的解决方案。

缓存雪崩


缓存雪崩是指在某一时期,大量的缓存同时失效或者redis服务器宕机,导致大量的请求到达数据库,带来巨大的压力。

解决方案:

给不同的缓存添加不同的过期时间(常用的数据ttl更长,冷门的ttl更短)
利用rediis集群提高服务的可用性,防止宕机
给缓存业务添加降级限流策略
给业务添加多级缓存,nginx缓存+redis缓存+其他缓存


缓存击穿


缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击

和雪崩的区别就是不是大量的key过期,redis还是正常状态,但数据库却崩了。

常用解决方案:

互斥锁:同时只让一个线程查询数据库,其他的都等待从缓存中取,实现简单,强一致性,但性能差,可用性变低了。
逻辑过期:永久存储该数据,但在数据中额外存储一个逻辑的过期时间。取到该数据时检测到过期了,那还是返回该数据,只不过会再新开一个线程去同步数据库的新数据到缓存。最终一致性,实现复杂。
预先设置热门数据:在redis高峰访问之前,把热门的数据提前存到redis里,并加大热门数据的ttl
实时调整:监控热门数据,调整key的ttl

;