缓存-讲解大纲
-
性能优化 (减少数据库or磁盘负担、降低传输频率)
-
提高可用性(服务即使短暂停止,仍可从缓存中拿数据)
内存(需频繁获取的数据)
磁盘(需大量缓存的数据)
-
缓存一致性问题:
描述:源数据频繁更新,导致缓存数据不一致
解决方案:依赖于缓存过期策略与淘汰策略
- 缓存并发:
描述:缓存在更新时,多个用户在访问缓存,导致用户获取的数据不一致
解决方案:更新时加锁
- 缓存击穿:
描述:高频率查询不存在的值,从而越过缓存查询数据库,造成数据库查询负担
解决方案:通过Map过滤
- 缓存雪崩:
描述:缓存如果同时过期,导致大量查询又从数据库中获取
解决方案:缓存过期的时间均匀分散
- 适用:
- 写少读多
- 一致性要求不严格
- 大数据量
- 不适用:
- 以上反之
先通过一个例子来看一下,什么是N+1问题?
list()获得对象:
| Java List ls = (List)session.createQuery("from Student") .setFirstResult(0).setMaxResults(30).list(); Iterator stus = ls.iterator(); for(;stus.hasNext();) { Student stu = (Student)stus.next(); System.out.println(stu.getName());} |
|---|
如果通过list()方法来获得对象,毫无疑问,hibernate会发出一条sql语句,将所有的对象查询出来。
| SQL Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ? |
|---|
那么,再来看看iterator()这种情况
iterator()获得对象
Java /** * 如果使用iterator方法返回列表,对于hibernate而言,它仅仅只是发出取id列表的sql * 在查询相应的具体的某个学生信息时,会发出相应的SQL去取学生信息 * 这就是典型的N+1问题 * 存在iterator的原因是,有可能会在一个session中查询两次数据,如果使用list每一次都会把所有的对象查询上来 * 而是要iterator仅仅只会查询id,此时所有的对象已经存储在一级缓存(session的缓存)中,可以直接获取 */ Iterator stus = (Iterator)session.createQuery("from Student").setFirstResult(0).setMaxResults(30).iterate(); for(;stus.hasNext();) { Student stu = (Student)stus.next(); System.out.println(stu.getName()); } |
|---|
在执行完上述的测试用例后,来看看控制台的输出,看会发出多少条 sql 语句:
| SQL Hibernate: select student0_.id as col_0_0_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 张三 Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 李四 Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 王五......... |
|---|
可以看到,当如果通过iterator()方法来获得对象的时候,hibernate首先会发出1条sql去查询出所有对象的 id 值,如果需要查询到某个对象的具体信息的时候,hibernate此时会根据查询出来的 id 值再发sql语句去从数据库中查询对象的信息,这就是典型的 N+1 的问题。
那么这种 N+1 问题如何解决呢,其实只需要使用 list() 方法来获得对象即可。但是既然可以通过 list() 就不会出现 N+1的问题,那么为什么还要保留 iterator()这种形式呢?考虑这样一种情况,如果需要在一个session当中要两次查询出很多对象,此时如果写两条 list()时,hibernate此时会发出两条 sql 语句,而且这两条语句是一样的,但是如果第一条语句使用 list(),而第二条语句使用 iterator()的话,此时也会发两条sql语句,但是第二条语句只会将查询出对象的id,所以相对应取出所有的对象而已,显然这样可以节省内存,而如果再要获取对象的时候,因为第一条语句已经将对象都查询出来了,此时会将对象保存到session的一级缓存中去,所以再次查询时,就会首先去缓存中查找,如果找到,则不发sql语句了。这里就牵涉到了接下来这个概念:hibernate的一级缓存。
先看看一级缓存
| Java /** * 此时会发出一条sql,将所有学生全部查询出来,并放到session的一级缓存当中 * 当再次查询学生信息时,会首先去缓存中看是否存在,如果不存在,再去数据库中查询 * 这就是hibernate的一级缓存(session缓存) */ List stus = (List)session.createQuery("from Student").setFirstResult(0).setMaxResults(30).list(); Student stu = (Student)session.load(Student.class, 1); |
|---|
| Apache Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ? |
|---|
此时hibernate仅仅只会发出一条 sql 语句,因为第一行代码就会将整个的对象查询出来,放到session的一级缓存中去,当我如果需要再次查询学生对象时,此时首先会去缓存中看是否存在该对象,如果存在,则直接从缓存中取出,就不会再发sql了,但是要注意一点:
hibernate的一级缓存是session级别的,所以如果session关闭后,缓存就没了,此时就会再次发sql去查数据库。
session关闭以后,一级缓存就不存在了,所以如果再查询的时候,就会再发sql。要解决这种问题,应该怎么做呢?这就要来配置hibernate的二级缓存了,也就是sessionFactory级别的缓存。
关于二级缓存的配置可以参考博客:www.cnblogs.com/tianyuchen/…
因为二级缓存是sessionFactory级别的缓存,当session关闭以后,再去查询对象的时候,此时hibernate首先会去二级缓存中查询是否有该对象,有就不会再发sql了。
| D public class TestSecondCache { @Test public void testCache1() { Session session = null; try { session = HibernateUtil.openSession(); Student stu = (Student) session.load(Student.class, 1); System.out.println(stu.getName() + "-----------"); } catch (Exception e) { e.printStackTrace(); } finally { HibernateUtil.close(session); } try { /** * 即使当session关闭以后,因为配置了二级缓存,而二级缓存是sessionFactory级别的,所以会从缓存中取出该数据 * 只会发出一条sql语句 */ session = HibernateUtil.openSession(); Student stu = (Student) session.load(Student.class, 1); System.out.println(stu.getName() + "-----------"); /** * 因为设置了二级缓存为read-only,所以不能对其进行修改 */ session.beginTransaction(); stu.setName("aaa"); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.close(session); } } |
|---|
| SQL Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? aaa----------- aaa----------- |
|---|
需要注意的是,二级缓存缓存的仅仅是对象,如果查询出来的是对象的一些属性,则不会被加到缓存中去。
- 哪些对象会被存放到缓存中?
配置了@Cache(usage = CacheConcurrencyStrategy.READ_WRITE, region = "POJO")
-
如何查看?
-
如何操控二级缓存?
通过smartbix.cache.CacheManager对象
- 如何清除缓存?
- 前端:远程调用util.remoteInvokeEx("ConfigClientService", "clearCache", []);
- 后端:smartbix.smartbi.util.clean.SmartbixEhCacheCleaner.doClean()
上面提到的二级缓存其实就是基于ehcache使用的,若没有学过则可以参考视频:java专题之缓存模块(ehcache/guava cache/自定义spring的CacheManager/自定义缓存)_哔哩哔哩_bilibili
我们产品中的对象池对象是**smartbi.pool.BaseSmartbiObjectPool,**其具体实现是如下图:
它是改写了commons-pool的org.apache.commons.pool.BaseKeyedObjectPool。
关于怎么理解对象池?类似于我们经常遇到线程池、连接池,它是将一系列的资源事先准备好放在一个地方,等需要的时候直接拿过去用。而用完之后再放回来。和我们平常的需要使用资源再创建的方式相比,这种池的方式节省了创建和销毁资源的这么一个过程。所以说,对于一些比较比较稀缺的资源或者创建和销毁影响系统性能的资源,采用池的方式可以有效的提高整体性能。
整个对象池的关键部分就三个:**工厂、对象、池,**之间的关系可以用下图来表示
关于工厂的核心方法包括:
Java //新增对象 Object makeObject(); //在他从池中借出的之前检测对象是否可用 boolean validateObject(PooledObject obj); //销毁对象 void destroyObject(PooledObject obj); |
|---|
关于池的核心方法包括:
| Java //从池中借走到一个对象。借走不等于删除。 Object borrowObject(); //把对象归还给对象池。归还不等于添加。 void returnObject(Object key, Object obj); //销毁一个对象。这个方法才会将对象从池子中删除,当然这其中最重要的就是释放对象本身持有的各种资源。 void invalidateObject(T obj) throws Exception; //往池中添加一个对象。池子里的所有对象都是通过这个方法进来的 //清空池 void clear() throws Exception, UnsupportedOperationException; |
|---|
关于redis的使用,若没有学过可以参考视频:Redis入门记(完结),Redis6零基础快速入门教程2022版_哔哩哔哩_bilibili
补充
Request的attribute也可以充当缓存的作用,例如在产品中的PageRefreshRequestCache类中,存放的各个Request的键。
| PHP public class PageRefreshRequestCache { /** * BUSINESS_TABLE_FIELD_KEY */ public static final String BUSINESS_TABLE_FIELD_KEY = "BUSINESS_TABLE_FIELD.KEY"; /** * FIELD_NODES_KEY */ public static final String FIELD_NODES_KEY = "BASE_DATASET_FIELD_NODES.KEY"; /** * TABLE_SHORT_NAME_KEY */ public static final String TABLE_SHORT_NAME_KEY = "TABLE_SHORT_NAME.KEY"; /** * GET_DATASOURCE_BY_ID_KEY */ public static final String GET_DATASOURCE_BY_ID_KEY = "GET_DATASOURCE_BY_ID"... } |
|---|
同理,session的attribute也可以充当缓存对象。但需要注意的是request的缓存仅仅只是在当前线程起作用,如果遇到多线程的读写时,则可能会出现问题。而session是全局的,无需考虑这一点。
可参考EPPR-52129
着重讲解上述聊到的基础技术在smartbi中缓存的运用:
Smartbi在构建数据集定义对象时,需要进行一系列的加载、解析过程,会较为耗时,因此所有的数据集定义对象生成后会缓存起来,下次使用时直接从缓存中获取。数据集定义对象缓存池的大小可以在:系统选项->缓存设置->数据集定义对象池设置。
大致流程可如下:
数据集定义对象(BusinessViewBO)获取的主要流程为:
(1) 尝试从session中取出BusinessViewBO,如存在,则存入request中。
(2) 尝试从request中取出BusinessViewBO,如存在则返回BusinessViewBO,进入第(5)步;如 不存在继续第(3)步。
(3) 尝试从缓冲池中取出BusinessViewBO,如存在,则从缓冲池中取出,存入request中,并返回BusinessViewBO,进入第(5)步;如不存在继续第(4)步。
(4) 创建BusinessViewBO对象,然后存入request,并返回BusinessViewBO,进入第(5)步。
(5) BusinessViewBO对象使用完毕后,返还到缓冲池中,并清除request缓存的BusinessViewBO,结束。
下面介绍数据集定义缓存的实现逻辑。
1、打开报表、刷新数据等操作均需要获取到数据集对象。获取数据集时,首先会进入MetaDataRuntimeContext类的searchBusinessView方法,根据当前的数据集ID查找。
2、根据数据集ID,先从session中查找数据集定义对象。
3、如果在session中找到数据集定义的缓存对象,则缓存至当前request(请求)的缓存对象中,后面再通过request的缓存获取。(当前request的缓存对象是会随着请求结束而销毁的,之所以缓存到当前请求中,是因为在一个请求内,可能会存在多个地方需要获取数据集定义的地方,如加载一个灵活分析,构建报表对象时,需要数据集定义对象,构建参数对象时,也需要数据集定义对象,而构建报表对象和参数对象是在一个请求中完成的)
4 如果在request缓存中没有找到对应的BusinessViewBO(数据集定义对象),则需要到BusinessViewBOPool(数据集定义对象缓存池)中获取(借用,后面使用完需要还的)。
5、如果对应的BusinessViewBO对象还未被加载过,那么肯定无法通过BusinessViewBOPool缓存池中直接获取。此时会调用BaseSmartbiKeyedPoolableObjectFactory的makeObject方法创建BusinessViewBO对象。
6、当获取到的BusinessViewBO使用完毕后(一般是在请求结束时),会将BusinessViewBO对象返还到缓存池中。具体逻辑为:请求结束时,会调用StateModule的doEndRequest方法,其中会调用IRequestListner的doEndRequest方法。
7、最后会进入BaseSmartbiObjectPool的onEndRequest方法,并调用returnObject返还BusinessViewBO对象到缓存池中,这样即可在下一次使用时,直接从缓存池中获取。
8 通过以上的缓存逻辑,我们可以了解到,目前没有任何机制可以禁用数据集定义的缓存,如果需要清除缓存,只能通过外部调用清除服务器缓存的方法清除。或者更新数据集定义时,会触发清除数据集定义缓存。当更新数据集定义时,会进入DAOModule类的update方法,调用对象池变更监听器接口IObjectPoolChangeListener的update方法。
而Smartbi对象缓存池实现类BaseSmartbiObjectPool,已经通过内部类实现了IObjectPoolChangeListener接口,并实现update方法,根据数据集ID清除指定数据集定义缓存。
数据集从数据库获取到数据的过程受多种因素影响,经常会出现慢的问题。如:数据量过大、数据库服务器并发访问过大、SQL过于复杂等。为解决慢的问题,会通过缓存策略,以便尽量减少重复获取数据的次数,从而减轻服务器压力。
Smartbi的数据集数据主要使用HSQL(内存数据库)缓存,支持用户设置,可以在:系统选项》缓存》业务数据缓冲池 中设置。
数据集获取数据的大致流程为:
(1) 判断数据是否已经存在HSQL内存数据库中,如果不存在进入第(2)步;如果存在进入第(4)步。
(2) 判断数据是否已经存在HSQL文件中,如果存在并且数据集启用了缓存进入第(4)步;否则进入第(3)步。
(3) 从原始数据库获取数据,然后判断是否启用HSQL文件缓存,启用了则将数据插入HSQL文件中,如果没有启用则将数据插入HSQL内存数据库中,进入第(4)步。
(4) 判断是否启用HSQL文件缓存,如果启用了进入第(5)步;如果没有启用进入第(6)步。
(5) 从HSQL文件中读取数据,结束。
(6) 从HSQL内存数据库中读取数据,如果数据集没有启用缓存,则清除对应HSQL内存数据库的数据,结束。
下面介绍数据集数据缓存的具体实现逻辑。
1、数据集刷新数据时,首先会进入SQLResultStore的getGridData方法。
2、接着进入DBSQLResultStore的getGridDataInternal方法。首先判断获取的页数是否在内存数据库(HSQL)中。如果数据已经缓存在HSQL中,则直接返回,然后继续通过当前数据集的输出字段、告警信息等拼接HSQL执行的SQL语句,最后执行拼接的SQL,从HSQL中获取缓存的数据。
3、如果数据没有缓存在HSQL中,则继续判断是否缓存在HSQL的文件中(同时需要判断数据集是否启用缓存),如果存在,则同样返回,然后继续通过当前数据集的输出字段、告警信息等拼接HSQL执行的SQL语句,最后执行拼接的SQL,从HSQL文件中获取缓存的数据。(是否使用HSQL文件缓存数据,需要在:系统选项》数据集》文件缓存设置中设置)
4、如果数据没有缓存在HSQL文件中,则直接到原始数据库中获取,最终即进入DBSQLResultStore的executeInDatabaseInner方法中,根据当前数据集的输出字段、参数等信息拼接SQL,最后执行SQL,从原始数据库中获取数据。
5、从原始数据库获取到数据后,立即将数据插入到HSQL数据库或HSQL文件中,然后返回总行数,最终依然按照上面的步骤从HSQL数据库或HSQL文件中读取数据。
6、当没有启用数据集缓存时,程序会调用DBSQLResultStore的close方法,如果没有启用HSQL文件缓存,则会清除HSQL中缓存的数据。(注意,当启用HSQL文件缓存时,不会另外执行清除HSQL文件的操作,因为在判断数据是否存在HSQL文件中时,同时判断了数据集是否启用缓存)
从上面的实现逻辑来看,Smartbi数据集的数据是缓存在HSQL或HSQL文件中的,只要数据集不启用缓存,每次数据获取完成后都会清除HSQL中对应的缓存数据。而只要启用了缓存,数据就一定会缓存在HSQL或HSQL文件中。
这里先提出一个问题,数据集的缓存是否是所有用户共用的?答案是肯定的,但是要说明这个问题,我们需要先了解清楚上面进行数据集数据缓存所使用的DBSQLResultStore对象是如何生成的,因为数据集是否启用缓存,与DBSQLResultStore的生成方式有着紧密联系。
1、打开报表时,会先获取到对应的BusinessViewBO(数据集定义对象,前面已说明获取逻辑),然后根据当前报表对象(不同的报表,会使用不用的对象,如:灵活分析-SimpleReportBO,仪表分析-DashboardBO)和BusinessViewBO对象构建BusinessViewState(数据集状态对象)。
2、获取到BusinessViewState对象后,再根据数据集ID、BusinessViewState等构建DBSQLResultStoreKey对象。然后判断数据集是否启用缓存:如果没有启用,则直接新建一个DBSQLResultStore对象;如果启用了缓存,则使用DBSQLResultStoreKey到DBSQLResultStore对象池(DBSQLResultStorePool)中获取DBSQLResultStore对象。
3、启用缓存时,最后会进入BaseSmartbiObjectPool中的borrowRequestObject方法,其中会将借用到的DBSQLResultStore对象缓存到对象中。
4、当DBSQLResultStore使用完毕后,(一般是在请求结束时),会将DBSQLResultStore对象返还到缓存池中。具体逻辑为:请求结束时,会调用StateModule的doEndRequest方法,其中会调用IRequestListner的doEndRequest方法。
5、最后会进入BaseSmartbiObjectPool的onEndRequest方法,然后调用returnObject返还DBSQLResultStore对象到缓存池中,这样即可在下一次使用时,直接从缓存池中获取。
6、如果报表关闭时,会尝试获取到DBSQLResultStore对象,执行其close方法,以清除HSQL的缓存。而如果启用了缓存,DBSQLResultStore对象会在每次刷新完数据后返还给对象池,此时将无法获取到DBSQLResultStore对象,则不执行清除HSQL缓存的操作。
从整个DBSQLResultStore对象的获取逻辑中,我们可以了解到,数据集数据缓存与数据集和报表是绑定在一起的(即缓存的KEY值是由数据集和报表共同产生的),而与用户等其他因素是无关的,因此可以支持所有用户在打开同一张报表时,共用缓存。
按次抽取是结合用户的登录生成的Session及查询的参数组合进行抽取。
使用场景:
-
有些客户希望实时数据,但又无法使用直连模式(如数据模型中含有存储过程模型表)。
-
数据模型中一部分数据和用户信息密切相关(如权限等),但又不能使用直连模式。
可以参考SMS-37158
导致缓存失效的原因有哪些?
可以参考
思路:
如何解决?
从上述可以看出,以上都是前面提到的【缓存并发】而导致的,解决方案就是加锁控制。
1 请追踪产品中的【清空缓存】,清空了哪些方面的缓存?