Hibernate缓存以及哪些操作会向缓存中读取和存放数据

2023-11-05

Hibernate缓存

Hibernate有两级缓存,分别是一级缓存和二级缓存。一级缓存也叫Session级缓存,默认情况下就可以用,无需配置。一级缓存生命周期由Session对象决定,Session对象关闭,一级缓存也就消失。二级缓存也叫SessionFactory级缓存,需要配置后才能使用。二级缓存的生命周期比一级缓存的生命周期长,由SessionFactory对象决定,SessionFactory对象关闭,二级缓存也就消失。

哪些操作会向缓存中读取和存放数据?

测试之前先说一下Hibernate访问数据库,Hibernate对传统的JDBC进行了封装,一般我们访问数据库无非就是增、删、改、查这四个操作,而这四个操作均通过各自的SQL语句完成,所以Hibernate对数据库进行这四个操作时也离开不了SQL语句,如果我们配置Hibernate时配置了show_sql这个属性的话,一旦Hibernate对数据库进行了增、删、改、查操作的话,控制台就会将执行的SQL语句打印出来,这也就是下面我们为什么能将控制台有没有打印SQL语句作为Hibernate有没有访问数据库的原因。下面开始测试:

测试环境:
系统:Windows 10
JDK:1.8.102
Hibernate:5.2.10
软件:MyEclipse 2016

我们先看查询,查询我们常见的有get()、load()以及HQL中的Query对象的list()方法(后面为方便,我们称为query.list())和uniqueResult()方法(后面为方便,我们称为query.uniqueResult())这些方法。

先上结果:
get()、load()均会向缓存中存放以及读取数据,而query.list()和query.uniqueResult()会向缓存存放数据但不会从缓存中读取数据

get()方法:

测试之前先说一下get()方法是怎么查找数据的,它会先到一级缓存中去找,没有的话,它会到二级缓存中继续找,二级缓存中还没有的话,就会立马生成相应的查询SQL,并把这个SQL发送到数据库,到数据库中去找编号为7499的员工,如果数据库中也没找到的话,就会返回null。如果数据库中有的话就会返回查询结果,在它返回查询结果的同时,它还会把查到的结果放进缓存中(一二级缓存都会放)。

测试get()方法代码:

    // get()会向缓存中存放和读取数据测试
    public static void testGetCache(Session session){
        try {
            Employee emp6=session.get(Employee.class, 7499);
            System.out.println(emp6);
            Employee emp7=session.get(Employee.class, 7499);
            System.out.println(emp7);
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            session.close();
        }
    }

运行结果:

Hibernate: 
    select
        employee0_.empno as empno1_0_0_,
        employee0_.ename as ename2_0_0_,
        employee0_.job as job3_0_0_,
        employee0_.sal as sal4_0_0_,
        employee0_.deptno as deptno5_0_0_,
        employee0_.hiredate as hiredate6_0_0_,
        employee0_.comm as comm7_0_0_,
        employee0_.mgr as mgr8_0_0_ 
    from
        SCOTT.Emp employee0_ 
    where
        employee0_.empno=?
员工姓名:ALLEN  员工编号7499
员工姓名:ALLEN  员工编号7499

代码中我们查询编号7499的员工查了2次,但运行结果中只打印了第一次查询的SQL语句,说明第一次是到数据库中查的,第二次没有去数据库中查,但第二次却依然查到了编号7499的员工,原因是因为Hibernate的缓存机制,第一次找到编号为7499的员工到后就会把7499的员工信息放进缓存中,当我们第二次再次查找的7499的员工信息时,由于在缓存中已经找到了,所以就直接返回结果,就不会到数据库中去查了,因此控制台并没有打印第二次的SQL语句。也就是说第二次查询是从缓存中查(读取)出来的。所以结果很明显:get()会向缓存中存放以及读取数据。
get()测试结果:get()会向缓存中存放以及读取数据

load()方法:

load()和get()查找数据的方式差不多,但又有点区别。load()也是先到一级缓存中去找,没找到的话继续到二级缓存中找,二级缓存中还没找到的话,它会返回一个查询结果的代理对象,当后面我们用到查询结果时,这个时候它才会生成相应的SQL语句,并把SQL发送到数据库,到数据库中去找,你后面如果没用到查询结果的话它不会生成查询的SQL,不会到数据库中去找。换句话说,就是你什么时候用到查询结果,他就什么时候去数据库中找,如果在数据库中没找到的话,就会报错,会报org.hibernate.ObjectNotFoundException的异常,导致程序中断。如果数据库中有的话就会返回查询结果,在它返回查询结果的同时,它还会把查到的结果放进缓存中(一二级缓存都会放)

测试load()方法代码:

    // load()会向缓存中存放和读取数据测试
    public static void testLoadCache(Session session){
        try {
          Employee emp6=session.get(Employee.class, 7369);
          System.out.println(emp6);
          Employee emp7=session.get(Employee.class, 7369);
          System.out.println(emp7);
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            session.close();
        }
    }

运行结果:

Hibernate: 
    select
        employee0_.empno as empno1_0_0_,
        employee0_.ename as ename2_0_0_,
        employee0_.job as job3_0_0_,
        employee0_.sal as sal4_0_0_,
        employee0_.deptno as deptno5_0_0_,
        employee0_.hiredate as hiredate6_0_0_,
        employee0_.comm as comm7_0_0_,
        employee0_.mgr as mgr8_0_0_ 
    from
        SCOTT.Emp employee0_ 
    where
        employee0_.empno=?
员工姓名:SMITH  员工编号7369
员工姓名:SMITH  员工编号7369

代码中我们同样查询编号7369的员工查了2次,但运行结果中只打印了第一次查询的SQL语句,说明第一次是到数据库中查的,第二次没有去数据库中查,但第二次却依然查到了编号7369的员工,原因也是因为Hibernate的缓存机制,第一次找到编号为7369的员工后就会把7369的员工信息放进了缓存,第二次查找的7369的员工信息是从缓存中查(读取)出来的。所以load()也会向缓存中存放以及读取数据。
load()方法测试结果:load()会向缓存中存放以及读取数据

query.list()方法

测试query.list()方法代码:

    // 测试query.list()会向缓存中存放数据但不会从缓存的读取数据
    public static void testQueryListCache(Session session) {
        try {
            String hql="from Employee";
            Query query=session.createQuery(hql);
            List<Employee> list=query.list();
            for (int i=0;i<2;i++) {
                System.out.println("员工编号:"+list.get(i).getEmpNo()+"   员工姓名:"+list.get(i).getEmpName());
            }
            System.out.println("------------------------------------------");
            Employee emp=(Employee)session.get(Employee.class, 7654);
            System.out.println("员工编号:"+emp.getEmpNo()+"   员工姓名:"+emp.getEmpName());
            System.out.println("------------------------------------------");
            String hql2="from Employee";
            Query query2=session.createQuery(hql2);
            List<Employee> list2=query2.list();
            for (int i=0;i<2;i++) {
                System.out.println("员工编号:"+list2.get(i).getEmpNo()+"   员工姓名:"+list2.get(i).getEmpName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            session.close();
        }
    }

运行结果:

Hibernate:
select
employee0_.empno as empno1_0_,
employee0_.ename as ename2_0_,
employee0_.job as job3_0_,
employee0_.sal as sal4_0_,
employee0_.deptno as deptno5_0_,
employee0_.hiredate as hiredate6_0_,
employee0_.comm as comm7_0_,
employee0_.mgr as mgr8_0_
from
SCOTT.Emp employee0_
员工编号:20 员工姓名:empSu2
员工编号:19 员工姓名:empSave2

员工编号:7654 员工姓名:MARTIN

Hibernate:
select
employee0_.empno as empno1_0_,
employee0_.ename as ename2_0_,
employee0_.job as job3_0_,
employee0_.sal as sal4_0_,
employee0_.deptno as deptno5_0_,
employee0_.hiredate as hiredate6_0_,
employee0_.comm as comm7_0_,
employee0_.mgr as mgr8_0_
from
SCOTT.Emp employee0_
员工编号:20 员工姓名:empSu2
员工编号:19 员工姓名:empSave2

测试代码中我们用query.list()方法去查询所有员工的编号和姓名查了两次(为了方便看结果,循环打印员工的编号和姓名时只循环了2次),分别在第一条虚线前和第二条虚线后。两条虚线之间我们用get()的方式查找了其中一名员工,看两条虚线间有没有打印get()查询员工的SQL语句来验证第一次query.list()查询后,有没有向缓存中存放数据。

运行结果可以看出,控制台将这两次query.list()查询所有员工的SQL语句都打印了出来,说明两次query.list()查询都是到数据库中查找的。两次query.list()查询之间我们用get()去查询了编号为7654的员工,运行结果显示我们查到了该员工的信息,但控制台却没有打印查询该员工信息的SQL语句,说明get()没有到数据库中去查,get()查到的编号为7654的员工信息不是从数据库中查到的,由于get()查找数据的顺序是先从缓存中找再到数据库中找,但我们用get()却查到了编号为7654的员工,说明get()查到的数据是从缓存中查到(读取到)的,观察整段测试代码,get()查询之前我们只进行了query.list()查询操作,所以很明显缓存中的信息是query.list()查到结果后放进去的,这就说明了query.list()会向缓存中存放数据,此时缓存中已经有了所有员工信息,但get()后面的query.list()查询操作在缓存中已经有要查找的信息时依旧是到数据库中查的,所以会发现query.list()并不会从缓存中查找(读取)数据。综上:测试query.list()会向缓存中存放数据但不会从缓存的读取数据
query.list()测试结果:query.list()会向缓存中存放数据但不会从缓存的读取数据

query.uniqueResult()方法:

测试query.uniqueResult()代码:

    //测试query.uniqueResult()会向缓存中存放数据但不会从缓存的读取数据
    public static void testQueryUniqueResultCache(Session session) {
        try {
            String hql="from Employee where empNo=7876";
            Query query=session.createQuery(hql);
            Employee emp=(Employee)query.uniqueResult();
            System.out.println("员工编号:"+emp.getEmpNo()+"   员工姓名:"+emp.getEmpName());
            System.out.println("-------------------------------------------------");
            //通过get()来测试query.list()有没有向缓存中存放数据
            Employee emp3=(Employee)session.get(Employee.class, 7876);
            System.out.println("员工编号:"+emp3.getEmpNo()+"   员工姓名:"+emp3.getEmpName());
            System.out.println("-------------------------------------------------");
            String hql2="from Employee where empNo=7876";
            Query query2=session.createQuery(hql2);
            Employee emp2=(Employee)query.uniqueResult();
            System.out.println("员工编号:"+emp2.getEmpNo()+"   员工姓名:"+emp2.getEmpName());
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            session.close();
        }
    }

运行结果:

Hibernate:
select
employee0_.empno as empno1_0_,
employee0_.ename as ename2_0_,
employee0_.job as job3_0_,
employee0_.sal as sal4_0_,
employee0_.deptno as deptno5_0_,
employee0_.hiredate as hiredate6_0_,
employee0_.comm as comm7_0_,
employee0_.mgr as mgr8_0_
from
SCOTT.Emp employee0_
where
employee0_.empno=7876
员工编号:7876 员工姓名:ADAMS

员工编号:7876 员工姓名:ADAMS

Hibernate:
select
employee0_.empno as empno1_0_,
employee0_.ename as ename2_0_,
employee0_.job as job3_0_,
employee0_.sal as sal4_0_,
employee0_.deptno as deptno5_0_,
employee0_.hiredate as hiredate6_0_,
employee0_.comm as comm7_0_,
employee0_.mgr as mgr8_0_
from
SCOTT.Emp employee0_
where
employee0_.empno=7876
员工编号:7876 员工姓名:ADAMS

与上面的测试query.list()类似,我们用query.uniqueResult()查编号为7876的员工查了两次,分别是在第一条虚线前和 第二条虚线后。两条虚线中间我们用get()再次查找编号7876的员工,看两条虚线之间有没有打印get()查询的SQL语句来判断query.uniqueResult()有没有向缓存中放入数据。

结果显示两条虚线之外均有打印查询的SQL,说明两次query.uniqueResult()查询都是到数据库中去查的,从运行结果来看,两条虚线之间并没有打印SQL语句,说明我们用get()查找员工7876时并没有到数据库中去查,但是依然查到了员工信息,根据get()查找数据的顺序结合此时get()并没有到数据库中去查,可以知道,get()查到员工信息是从缓存中查(读取)到的,依旧是观察整段测试代码,get()查询员工前面除了query.uniqueResult()查询操作外并没有进行其它任何操作,所以,很显然,缓存中的员工信息是前面query.uniqueResult()查到结果后放进去的。到这就说明了query.uniqueResult()会向缓存中存放数据。
再来,结合get()后面的query.uniqueResult()查询操作,在缓存中已经有要查找的员工时,却还是到数据库中去查找。所以得出结论:query.uniqueResult()不会从缓存的读取数据。
综合就是:query.uniqueResult()会向缓存中存放数据但不会从缓存的读取数据

测试结果:query.uniqueResult()会向缓存中存放数据但不会从缓存的读取数据

说完了查询,我们说添加,添加我们常见的有save()和saveOrUpdate()
先上结果:
save()和saveOrUpdate()进行添加时,均会向缓存中存放数据。

save()方法:

测试save()方法代码

    //测试save()会向缓存中存放数据
    public static void testSaveCache(Session session) {
        try {
            Transaction tr=session.beginTransaction();
            Employee emp1=new Employee();
            emp1.setEmpName("empSave2");
            session.save(emp1);
            System.out.println("添加的对象的主键为:"+emp1.getEmpNo());
            Employee emp2=(Employee)session.get(Employee.class,emp1.getEmpNo());
            System.out.println("员工姓名:"+emp2.getEmpName());
            tr.commit();//只有当这句代码执行时,才会向数据库发送sql
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            session.close();
        }
    }

运行结果:

Hibernate: 
    select
        hibernate_sequence.nextval 
    from
        dual
添加的对象的主键为:19
员工姓名:empSave2
Hibernate: 
    insert 
    into
        SCOTT.Emp
        (ename, job, sal, deptno, hiredate, comm, mgr, empno) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?)

从代码可以看出,我们在tr.commit();这句代码之前我们又用get()查询了当前添加的对象,此时tr.commit();还没有执行,也就是事务还没提交,所以对象这个时候还没存进数据库,按理说我们应该查不到记录。

但从运行结果中可以看出,我们查到了数据,而且,控制台没有打印相关的查询SQL,说明我们用get()查找的员工信息不是从数据库中找的,结合get()查找数据是先从缓存再到数据库中找的顺序,所以我们能得出,查到的数据是从缓存中查到的,而在我们get()查询之前除了save()操作并没有其它操作,因此,缓存中的数据是save()放的,也就是说在我们调用save()添加时它会把添加的信息放进缓存里。
save()测试结果:save()会向缓存中存放数据

saveOrUpdate()方法:

测试saveOrUpdate()方法代码

    //测试saveOrUpdate()进行添加时会向缓存中存放数据
    public static void testSaveOrUpdateCache(Session session) {
        try {
            Transaction tx=session.beginTransaction();
            Employee emp3=new Employee();
            emp3.setEmpName("empSu2");
            session.saveOrUpdate(emp3);
            System.out.println("添加的对象的主键为:"+emp3.getEmpNo());
            Employee emp4=(Employee)session.get(Employee.class,emp3.getEmpNo());
            System.out.println("员工姓名:"+emp4.getEmpName());
            tx.commit();//只有当这句代码执行时,才会向数据库发送sql
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            session.close();
        }
    }

运行结果:

Hibernate: 
    select
        hibernate_sequence.nextval 
    from
        dual
添加的对象的主键为:20
员工姓名:empSu2
Hibernate: 
    insert 
    into
        SCOTT.Emp
        (ename, job, sal, deptno, hiredate, comm, mgr, empno) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?)

和测试save()一样,我们在tx.commit();这句代码之前我们又用get()查询了当前添加的对象,此时tr.commit();还没有执行,也就是事务还没提交,所以对象还没存进数据库,按理说我们应该查不到记录。

运行结果也和上面save()的一样,控制台并没有打印相关的查询SQL说明,说明我们用get()查找的员工信息不是从数据库中找的,结合get()查找数据是先从缓存再到数据库中找的顺序,所以我们能得出,查到的数据是从缓存中查到的,而在我们get()查询之前除了saveOrUpdate()操作并没有其它操作,因此,缓存中的信息是saveOrUpdate()操作放进去的,所以,saveOrUpdate()也会向缓存中存放数据。
saveOrUpdate()测试结果:saveOrUpdate()会向缓存中存放数据

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Hibernate缓存以及哪些操作会向缓存中读取和存放数据 的相关文章

  • 密码学加密模式分类

    目录 电子密码本模式 Electronic Codebook ECB 密码分组链接模式 Cipher Block Chaining CBC 密文反馈模式 Cipher Feedback CFB 输出反馈模式 Output Feedback
  • RepBaseRepeatMaskerEdition下载

    开源的生物信息世界居然有这么个需要注册才能下载的工具 开源世界不是怎么方便怎么来吗 这个注册真的麻烦 这里上传了一个可以使用的版本 RepBaseRepeatMaskerEdition 20170127 tar gz 想转成fasta可以用
  • 程序员财富自由之路 自媒体篇

    疫情期间 很多人的收入多多少少都受到了影响 当然 有的人坐吃山空 也有的人收入翻倍 我仔细观察了一下这些收入翻倍的人 发现他们基本都离不开三个字 哪三个字 就是自媒体 自媒体是什么 自媒体是指以内容为中心 个人创作者以网络的形式进行内容传播
  • Linux文件下I/O基础详解

    视频地址 华清远见 https www bilibili com video BV1dz411B7vj p 1 文件基础 一组相关数据的有序集合 文件类型 常规文件 r 二进制文件 ASCII码文件 目录文件 d 字符设备文件 c 块设备文
  • Python->进程-线程->TCP服务器客户端-服务端->返回固定数据的静态web服务器-学习

    作者 芝士小熊饼干 系列专栏 Python 坚持天数 12天 获取进程id和进程的父id 进程名 导入包 import multiprocessing import time import os 创建任务 def task1 print f
  • Django-admin录入中文错误:Incorrect string value

    在Django自带后台中经常会出现编码错误 Incorrect string value xE7 xAE x80 xE5 x8D x95 for column message at row 1 需要修改admin表的中的编码 才能保证数据正
  • Rust- FFI (Foreign Function Interface)

    Foreign Function Interface FFI is a mechanism that allows code written in one language to call code written in another l
  • 手写ArrayStack底层 实现代码

    一 接口的定义 package p1 接口 public interface Stack

随机推荐

  • 华为OD机试真题B卷 Java 实现【统计字符】,附详细解题思路

    一 题目描述 输入一行字符 分别统计出包含英文字母 空格 数字和其它字符的个数 数据范围 输入的字符串长度满足 1 le n le 1000 1 n 1000 二 输入描述 输入一行字符串 可以有空格 三 输出描述 统计其中英文字符 空格字
  • ECharts画动态仪表盘+柱状图(ajax获取+循环画图)

    出来工作几个月了 整理下最近学的的东西 刚刚才开通博客 第一篇就先画仪表盘 柱状图 有什么写的不好的地方 请大家多多指教 jsp中首先要引用几个文件 xx xxx js 是我js代码存放的文件
  • C++标准库异常类

    C 标准库异常类继承层次中的根类为exception 其定义在exception头文件中 它是C 标准库所有函数抛出异常的基类 exception的接口定义如下 namespace std class exception public ex
  • 记一个复制黏贴的功能想法(黏贴剪切板中的数字自增,复制黏贴自增)

    起因 照例是要写起因的 起因非常之简单 不知道大家有没有遇到过需要输入连续的 id 101 id 110 这类数字的时候 这个时候能做的基本上是复制100 黏贴100 然后手动改 101 102 在我的脑海里 除了使用excel 其他没有很
  • 【华为机试真题 JAVA】执行时长-100

    编程题目 100分 执行时长 2021 2022 Q2考试题 时间限制 C C 1秒 其他语言 2秒 空间限制 C C 262144K 其他语言524288K 64bit IO Format lld 本题可使用本地IDE编码 不能使用本地已
  • 面对对象--结构体和类的区别

    一 面向对象 结构体和类的区别 结构体是一种值类型 而类是引用类型 值类型用于存储数据的值 引用类型用于存储对实际数据的引用 那么结构体就是当成值来使用的 类则通过引用来对实际数据操作 构使用栈存储 Stack Allocation 而类使
  • 利用腾讯云函数隐藏C2服务器

    1 简介 腾讯云函数 可以为企业和开发者提供无服务器执行环境 无需购买和管理服务器 只需要在腾讯云上使用平台支持的语言编写核心代码并设置代码运行的条件 即可在腾讯云基础设施上弹性 安全地运行代码 C2服务器所有流量通过腾讯云函数进行转发 由
  • Html04_input框中的value/key/placeholder到底是什么

    1 input框中的value值到底是什么 value 属性为 input 元素设定值 input标签有很多类型 也就是type 对于不同的输入类型 value 属性的用法也不同 以下是一些常用type的说明 text 文本框 input默
  • 513. Find Bottom Left Tree Value

    Given a binary tree find the leftmost value in the last row of the tree Example 1 Input 2 1 3 Output 1 Example 2 Input 1
  • Flutter:自定义组件的上下左右弹出层

    背景 最近要使用Flutter实现一个下拉菜单 需求就是 在当前组件下点击 其下方弹出一个菜单选项 如下图所示 实现起来 貌似没什么障碍 在Flutter中本身就提供了弹出层PopupMenuButton组件和showMenu方法 于是开搞
  • 复习之linux存储的基本管理

    一 实验环境的设定 1 实验环境的搭建 本节内容只需要一台虚拟机 westosa reset重置虚拟机 保证实验环境的纯净 配置网络实现ssh连接 重置虚拟机后 配置网络 设定ip 172 25 254 100 保证与主机可以通信 实现ss
  • Ant Design的layout布局 --- 根据路由配置渲染

  • python基础3——流程控制

    文章目录 一 操作符 1 1 比较操作符 1 2 逻辑操作符 1 3 成员操作符 1 4 身份操作符 二 流程控制 2 1 条件判断 2 2 循环语句 2 2 1 for循环 2 2 2 while循环 2 3 continue与break
  • 谷歌Colab云端部署Stable Diffusion 进行绘图

    系列文章目录 本地部署Stable Diffusion教程 亲测可以安装成功 Stable Diffusion界面参数及模型使用 文章目录 系列文章目录 前言 一 Colab是什么 二 操作步骤 1 找到对应的脚本 2 在谷歌Colab里执
  • wait WNOHANG 僵尸进程

    什么是僵尸进程 首先内核会释放终止进程 调用了exit系统调用 所使用的所有存储区 关闭所有打开的文件等 但内核为每一个终止子进程保存了一定量的信息 这些信息至少包括进程ID 进程的终止状态 以及该进程使用的CPU时间 所以当终止子进程的父
  • Apollo如何通知/订阅主题topic

    转自 https blog csdn net u012423865 article details 80024870 导读 众所周知 Apollo是基于ROS开发的 所以其底层也是基于消息的机制进行节点通信的 但是它在ROS的基础上做了一些
  • 渗透相关问题(一)

    1 什么是kali kali Linux是基于Debian的Linux发行版 最早是设计用于数字取证的操作系统 被称为BackTrack 基于ubuntu kali集成了精心挑选的渗透测试和安全审计的工具 供渗透测试和安全设计人员使用 也可
  • Qt 实现当某个变量的值发生改变时修改按钮控件的参数

    mywidget h ifndef MYWIDGET H define MYWIDGET H include
  • Edge 浏览器的扩展安装到 Chrome

    在 Edge 浏览器输入 edge version 按回车键后 可以看到 用户配置路径 xxx 路径下的 Extensions 文件夹就是 Edge 安装扩展的目录 找到需要安装到 Chrome 的插件的目录 如 C Users Admin
  • Hibernate缓存以及哪些操作会向缓存中读取和存放数据

    Hibernate缓存 Hibernate有两级缓存 分别是一级缓存和二级缓存 一级缓存也叫Session级缓存 默认情况下就可以用 无需配置 一级缓存生命周期由Session对象决定 Session对象关闭 一级缓存也就消失 二级缓存也叫