一.概述

1.简介

  • MyBatis是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis免除了几乎所有的JDBC代码以及设置参数和获取结果集的工作。MyBatis可以通过简单的XML或注解来配置和映射原始类型、接口和Java POJO(Plain Old Java Objects,普通老式Java对象)为数据库中的记录

2.maven构建

  • 将MyBatis相关依赖导入项目,pom.xml添加如下配置
    1. <dependency>
    2. <groupId>org.mybatis</groupId>
    3. <artifactId>mybatis</artifactId>
    4. <version>3.5.7</version>
    5. </dependency>
  • 将Mysql相关依赖导入
    1. <dependency>
    2. <groupId>mysql</groupId>
    3. <artifactId>mysql-connector-java</artifactId>
    4. <version>5.1.37</version>
    5. </dependency>
  • 将Junit相关依赖代入
    1. <dependency>
    2. <groupId>junit</groupId>
    3. <artifactId>junit</artifactId>
    4. <version>4.12</version>
    5. <scope>test</scope>
    6. </dependency>
  • 将log4j相关依赖导入
    1. <dependency>
    2. <groupId>log4j</groupId>
    3. <artifactId>log4j</artifactId>
    4. <version>1.2.17</version>
    5. </dependency>


log4j的配置文件名为log4j.xml,存放的位置是src/main/resources目录下:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
  3. <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
  4. <appender name="STDOUT" class="org.apache.log4j.ConsoleAppender">
  5. <param name="Encoding" value="UTF-8" />
  6. <layout class="org.apache.log4j.PatternLayout">
  7. <param name="ConversionPattern" value="%-5p %d{MM-dd HH:mm:ss,SSS}%m (%F:%L) \n" />
  8. </layout>
  9. </appender>
  10. <logger name="java.sql">
  11. <level value="debug" />
  12. </logger>
  13. <logger name="org.apache.ibatis">
  14. <level value="info" />
  15. </logger>
  16. <root>
  17. <level value="debug" />
  18. <appender-ref ref="STDOUT" />
  19. </root>
  20. </log4j:configuration>

二.相关概念

1.Mapper接口

MyBatis中的mapper接口相当于以前的dao。但是区别在于,mapper仅仅是接口,我们不需要提供实现类。

  • Mapper接口的取名应该是和映射文件名保持一致

  • 比如,某个实体类User,它的Mapper接口如下:

    1. public interface UserMapper{
    2. int insert();
    3. }
  • 在Mapper接口中可以提供一些抽象方法,用于增删改查

2.ORM思想

  • ORM是指(Object Relationship Mapping)对象关系映射

  • 其中

    • 对象:Java的实体类对象
    • 关系:关系型数据库
    • 映射:二者之间的对应关系
  • 体现 | Java概念 | 数据库概念 | | —- | —- | | 类 | 表 | | 属性 | 字段/列 | | 对象 | 记录/行 |

三.映射配置文件

1.文件结构

  • 命名规则:数据库表对应的类名+Mapper.xml

  • 一个映射文件对应一个实体类,对应一个表中的操作

  • 映射文件主要用于编写SQL、访问以及操作表中的数据

  • 映射文件存放位置是maven工程下的src/main/resources/mappers目录下

  • 映射配置文件要保证两个一致

    • mapper接口的全类名和映射文件的命名空间namespace保持一致
    • mapper接口中方法的方法名和映射文件中编写SQL的标签的id属性保持一致
  • 映射文件的简易结构如下 ```xml <?xml version=”1.0” encoding=”UTF-8” ?> <!DOCTYPE mapper
    1. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    2. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    SQL语句

  1. <a name="130be2de"></a>
  2. ## 2.映射配置文件标签详解
  3. -
  4. <insert>标签
  5. -
  6. 用于书写插入数据的SQL语句
  7. -
  8. id属性指定对应mapper接口的方法名
  9. -
  10. 范例
  11. ```xml
  12. <insert id="insertUser">
  13. SQL语句
  14. </insert>
  • 标签

    • 用于删除表中的数据

    • id属性指定对应mapper接口的方法名

    • 范例

      1. <delete id="deleteUser">
      2. SQL语句
      3. </delete>
  • 标签

    • 用于更新表中数据

    • id属性指定对应mapper接口中的方法名

    • 范例

      1. <update id="updateUser">
      2. SQL语句
      3. </update>
  • select emp_id,emp_name,age,gender,t_dept.dept_id,dept_name from t_emp left join t_dept on t_emp.dept_id = t_dept.dept_id where emp_id = #{empId}

    1. -
    2. **方式二:使用<association>标签**
    3. -
    4. <resultMap>配置
    5. ```xml
    6. <resultMap id="getEmpAndDeptByEmpIdResultMap" type="Emp">
    7. <id column="emp_id" property="empId"></id>
    8. <result column="emp_name" property="empName"></result>
    9. <result column="age" property="age"></result>
    10. <result column="gender" property="gender"></result>
    11. <association property="dept" javaType="Dept">
    12. <id column="dept_id" property="deptId"></id>
    13. <result column="dept_name" property="deptName"></result>
    14. </association>
    15. </resultMap>
    16. <select id="getEmpAndDeptByEmpId" resultMap="getEmpAndDeptByEmpIdResultMap">
    17. select emp_id,emp_name,age,gender,t_dept.dept_id,dept_name
    18. from t_emp left join t_dept
    19. on t_emp.dept_id = t_dept.dept_id where emp_id = #{empId}
    20. </select>
    • 方式三:使用分步查询

      • 配置
        查询员工信息: ```xml

    1. <br />根据员工的部门id查询部门信息
    2. ```xml
    3. <resultMap id="getDeptByDeptIdResultMap" type="Dept">
    4. <id column="dept_id" property="deptId"></id>
    5. <result column="dept_name" property="deptName"></result>
    6. </resultMap>
    7. <select id="getDeptByDeptId" resultMap="getDeptByDeptIdResultMap">
    8. select * from t_dept where dept_id = #{deptId}
    9. </select>

    7.一对多映射关系的处理

    • 这里一对多是指实体类中某个属性是由许多实体类构成的集合,如部门类中员工属性是一个List集合

    • 方式一:使用标签

      • 配置 ```xml

    1. -
    2. **方式二:使用分步查询**
    3. -
    4. <resultMap>配置
    5. <br />查询部门信息
    6. ```xml
    7. <resultMap id="getDeptAndEmpByDeptIdResultMap" type="Dept">
    8. <id column="dept_id" property="deptId"></id>
    9. <result column="dept_name" property="deptName"></result>
    10. <collection property="emps"
    11. select="com.liaoxiangqian.mapper.EmpMapper.getEmpByDeptId"
    12. column="dept_id">
    13. </collection>
    14. </resultMap>
    15. <select id="getDeptAndEmpByDeptId" resultMap="getDeptAndEmpByDeptIdResultMap">
    16. select * from t_dept where dept_id = #{deptId}
    17. </select>


    根据部门id查询员工信息

    1. <resultMap id="getEmpByDeptIdResultMap" type="Emp">
    2. <id column="emp_id" property="empId"></id>
    3. <result column="emp_name" property="empName"></result>
    4. <result column="age" property="age"></result>
    5. <result column="gender" property="gender"></result>
    6. </resultMap>
    7. <select id="getEmpByDeptId" resultMap="getEmpByDeptIdResultMap">
    8. select * from t_emp where dept_id = #{deptId}
    9. </select>

    8.分布查询的优点

    • 分布查询的优点是可以实现延迟加载

    • 延迟加载可以避免在分步查询中执行所有的SQL语句,节省资源,实现按需加载

    • 需要在核心配置文件中添加如下的配置信息

      1. <settings>
      2. <setting name="lazyLoadingEnabled" value="true"/>
      3. <setting name="aggressiveLazyLoading" value="false"/>
      4. </settings>
    • lazyLoadingEnabled表示全局的延迟加载开关,true表示所有关联对象都会延迟加载,false表示关闭

    • aggressiveLazyLoading表示是否加载该对象的所有属性,如果开启则任何方法的调用会加载这个对象的所有属性,如果关闭则是按需加载

    • 由于这个配置是在核心配置文件中设定的,所以所有的分步查询都会实现延迟加载,而如果某个查询不需要延迟加载,可以在collection标签或者association标签中的fetchType属性设置是否使用延迟加载,属性值lazy表示延迟加载,属性值eager表示立即加载

    9.动态SQL

    • if标签

      • if标签通过test属性给出判断的条件,如果条件成立,则将执行标签内的SQL语句

      • 范例

        1. <select id="getEmpByCondition" resultType="Emp">
        2. select * from t_emp where
        3. <if test="empName != null and empName != ''">
        4. emp_name = #{empName}
        5. </if>
        6. <if test="age != null and age != ''">
        7. and age = #{age}
        8. </if>
        9. <if test="gender != null and gender != ''">
        10. and gender = #{gender}
        11. </if>
        12. </select>
    • where标签

      • 考虑if标签中的范例出现的一种情况:当第一个if标签条件不成立而第二个条件成立时,拼接成的SQL语句中where后面连着的是and,会造成SQL语句语法错误,而where标签可以解决这个问题

      • 范例

        1. <select id="getEmpByCondition" resultType="Emp">
        2. select * from t_emp
        3. <where>
        4. <if test="empName != null and empName != ''">
        5. emp_name = #{empName}
        6. </if>
        7. <if test="age != null and age != ''">
        8. and age = #{age}
        9. </if>
        10. <if test="gender != null and gender != ''">
        11. and gender = #{gender}
        12. </if>
        13. </where>
        14. </select>
    • where标签只会在子标签返回任何内容的情况下才插入WHERE子句。而且,若子句的开头有多余的and或者or,where标签也会将它们去除,但是子句末尾的and或者or不能去除
    • trim标签

      • trim标签用于去掉或添加标签中的内容

      • trim标签常用属性

        1. prefix:在trim标签中的内容的前面添加某些内容
        2. prefixOverrides:在trim标签中的内容的前面去掉某些内容
        3. suffix:在trim标签中的内容的后面添加某些内容
        4. suffixOverrides:在trim标签中的内容的后面去掉某些内容
      • 用trim实现where标签范例相同的功能
        1. <select id="getEmpByCondition" resultType="Emp">
        2. select * from t_emp
        3. <trim prefix="where" prefixOverrides="and">
        4. <if test="empName != null and empName != ''">
        5. emp_name = #{empName}
        6. </if>
        7. <if test="age != null and age != ''">
        8. and age = #{age}
        9. </if>
        10. <if test="gender != null and gender != ''">
        11. and gender = #{gender}
        12. </if>
        13. </trim>
        14. </select>
    • choose、when、otherwise标签

      • 这三个标签是组合使用的,用于在多条件中选择一个条件,类似Java中的if…else if…else…语句

      • 范例

        1. <select id="getEmpByCondition" resultType="Emp">
        2. select * from t_emp where gender = #{gender}
        3. <choose>
        4. <when test="empName != null and empName != ''">
        5. and emp_name = #{empName}
        6. </when>
        7. <when test="age != null and age != ''">
        8. and age = #{age}
        9. </when>
        10. </choose>
        11. </select>
    • 当某个when标签的条件满足时将对应的SQL语句返回,如果都不满足并且有otherwise标签时,才会返回otherwise标签中的SQL语句
    • foreach标签

      • foreach标签允许指定一个集合或数组,并且对这个集合或数组进行遍历

      • foreach标签可以用的属性有

        1. collection:指定需要遍历的集合或数组
        2. item:当前遍历到的元素
        3. index:当前遍历到的元素的序号
        4. 当遍历的集合是Map类型时,index表示键,item表示值
        5. open:指定遍历开始前添加的字符串
        6. close:指定遍历开始后添加的字符串
        7. separator:指定每次遍历之间的分隔符
      • collection属性值注意事项

        • 如果遍历的是List时,属性值为list
        • 如果遍历的是数组时,属性值为array
        • 如果遍历的是Map时,属性值可以是map.keys()map.values()map.entrySet()
        • 除此之外,还可以在映射方法的参数中使用@Param()注解自定义collection属性值
      • 批量添加数据
        1. <insert id="addMoreEmp">
        2. insert into t_emp values
        3. <foreach collection="list" separator="," item="emp">
        4. (null,#{emp.empName},#{emp.age},#{emp.gender},null)
        5. </foreach>
        6. </insert>
    • 批量删除数据
      1. <delete id="deleteMoreEmp">
      2. delete from t_emp where emp_id in
      3. <foreach collection="array" item="empId" separator="," open="(" close=")">
      4. #{empId}
      5. </foreach>
      6. </delete>
    • sql标签

      • 用于记录一段通用的SQL语句片段,在需要用到该SQL语句片段的地方中通过include标签将该SQL语句片段插入

      • sql标签通过id属性唯一标识一个SQL语句片段,include标签通过refid属性指定使用某个SQL片段

      • 范例

        1. <sql id="item">
        2. emp_id,emp_name,age,gender,dept_id
        3. </sql>
        4. <select id="getEmpByEmpId" resultType="Emp">
        5. select <include refid="item"></include>
        6. from t_emp
        7. where emp_id = #{empId}
        8. </select>

    四.核心配置文件

    1.文件结构

    • 核心配置文件命名建议是mybatis-config.xml,无强制要求

    • 核心配置文件主要用于配置连接数据库的环境以及MyBatis的全局配置信息

    • 核心配置文件存放的位置是maven工程下的src/main/resources目录下

    • 简易结构如下,核心配置文件的标签不止这几个

      1. <?xml version="1.0" encoding="UTF-8" ?>
      2. <!--DTD约束-->
      3. <!DOCTYPE configuration
      4. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
      5. "http://mybatis.org/dtd/mybatis-3-config.dtd">
      6. <configuration>
      7. <!--
      8. 1.environments配置数据库的环境,环境可以有多个
      9. 2.default属性指定使用的环境
      10. -->
      11. <environments default="development">
      12. <!--
      13. 1.environment配置具体某个数据库的环境
      14. 2.id属性唯一标识这个环境
      15. -->
      16. <environment id="development">
      17. <!--
      18. 1.transactionManager设置事务管理方式
      19. 2.type属性取值有“JDBC|MANAGED”
      20. 3.JDBC指当前环境中使用的是JDBC中原生的事务管理方式,事务的提交或回滚需要手动处理
      21. 4.MANAGED指被管理,例如Spring中
      22. -->
      23. <transactionManager type="JDBC"/>
      24. <!--
      25. 1.dataSource配置数据源
      26. 2.取值有"POOLED|UNPOOLED|JNDI"
      27. 3.POOLED表示使用数据库连接池缓存数据库连接
      28. 4.UNPOOLED:表示不使用数据库连接池
      29. 5.JNDI表示使用上下文中的数据源
      30. -->
      31. <dataSource type="POOLED">
      32. <!--设置链接数据库的驱动-->
      33. <property name="driver" value="com.mysql.jdbc.Driver"/>
      34. <!--设置连接数据库的地址-->
      35. <property name="url" value="jdbc:mysql://localhost:3306/ssm"/>
      36. <!--设置连接数据库的用户名-->
      37. <property name="username" value="root"/>
      38. <!--设置连接数据库的密码-->
      39. <property name="password" value="lxq"/>
      40. </dataSource>
      41. </environment>
      42. </environments>
      43. <!--mappers用于引入映射的配置文件-->
      44. <mappers>
      45. <!--mapper用于指定某个映射文件,resource属性指定文件路径-->
      46. <mapper resource="mappers/UserMapper.xml"/>
      47. </mappers>
      48. </configuration>

    2.核心配置文件详解

    (1)标签顺序

    • 核心配置文件中configuration标签下的子标签要按照一定的顺序书写
    • properties => settings => typeAliases => typeHandlers => objectFactory => objectWrapperFactory => reflectorFactory => plugins => environments => databaseIdProvider => mappers

    (2)标签详解

    • 标签

      • 用于引入某个properties配置文件,是一个单标签

      • resource属性指定配置文件

      • 范例

        1. <properties resource="jdbc.properties" />
    • 标签

      • 用于为某个类的全类名设置别名,子标签是

      • 一个子标签对应设置一个类的别名

      • 子标签下有type和alias两个属性,type指定需要设置别名的类的全类名,alias指定别名

      • 如果只设置了type属性,那么默认的别名就是它的类名(不是全类名)而且不区分大小写

      • 如果想要设置某个包下所有类的别名,可以使用标签,用name属性指定包名

      • 范例

        1. <typeAliases>
        2. <typeAlias type="com.lxq.pojo.User" alias="User"></typeAlias>
        3. <package name="com.lxq.pojo"></package>
        4. </typeAliases>
    • MyBatis中内建了一些类型的别名,常见的有 | Java类型 | 别名 | | —- | —- | | int | _int或_integer | | Integer | int或integer | | String | string | | List | list | | Map | map |
    • 标签

      • 用于配置连接数据库时用到的各种属性,是一个单标签

      • 该标签有两个属性,一个是name指定属性名,另一个是value指定属性值

      • 如果不使用标签引入相关配置文件时,使用方式如下

        1. <property name="driver" value="com.mysql.jdbc.Driver" />
    • 如果使用标签引入相关的配置文件时,value属性可以写成如下形式
      1. <property name="driver" value="${jdbc.driver}" />


    其中配置文件的内容是

    1. jdbc.driver=com.mysql.jdbc.Driver


    注意:这里使用jdbc.driver来给键命名是因为核心配置文件中可能会引入其他的配置文件,如果使用driver来命名键的话有可能会跟其他配置文件中的键同名而产生冲突

    • 标签

      • 该标签用于引入映射文件

      • 每个映射文件使用子标签来表示,该子标签是一个单标签

      • 子标签使用属性resource来指定需要引入的映射文件

      • 如果想要将某个包下所有的映射文件都引入,可以使用标签,使用name属性来指定需要引入的包

      • 范例

        1. <mappers>
        2. <mapper resource="mappers/UserMapper.xml" />
        3. <package name="com.lxq.mapper" />
        4. </mappers>


    注意:使用包的形式引入映射文件需要满足两个条件,1.mapper接口所在的包和映射文件所在的包要一致;2.mapper接口名和映射文件名要相同

    五.相关API

    1.Resources

    • Resources类由MyBatis提供用于获取来自核心配置文件的输入流
    • 相关方法是:InputStream getResourceAsStream(String filepath),注意这是一个静态方法

    2.SqlSessionFactoryBuilder

    • SqlSessionFactoryBuilder类由MyBatis提供用于获取SqlSessionFactory的实例对象
    • 相关方法是:SqlSessionFactory build(InputStream is),该方法通过一个输入流返回了SqlSessionFactory对象

    3.SqlSessionFactory

    • SqlSessionFactory类由MyBatis提供用于获取SqlSession对象,每个基于MyBatis的应用都是以一个SqlSessionFactory的实例为核心的
    • 相关方法:SqlSession openSession()SqlSession openSession(boolean autoCommit),这两个方法都用于获取SqlSession对象,如果使用有参数的可以指定是否自动提交事务,没有指定参数的默认是不自动提交事务

    4.SqlSession

    • SqlSession类由MyBatis提供用于执行SQL、管理事务、接口代理

    • 常用方法 | 方法 | 说明 | | —- | —- | | void commit() | 提交事务 | | void rollback() | 回滚事务 | | T getMapper(Class aClass) | 获取指定接口的代理实现类 | | void close() | 释放资源 |

    • 除了以上常用方法外,SqlSession还有很多有关数据库增删改查的方法,但是这些方法繁琐而且不符合类型安全,所以使用getMapper()方法来获取一个Mapper接口的代理实现类来执行映射语句是个比较方便的做法

    5.最佳实践

    • SqlSessionFactoryBuilder
      这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。因此 SqlSessionFactoryBuilder 实例的最佳范围是方法范围(也就是局部方法变量)。你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但是最好还是不要让其一直存在以保证所有的 XML 解析资源开放给更重要的事情

    • SqlSessionFactory
      SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此  SqlSessionFactory 的最佳范围是应用范围。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式

    • SqlSession
      每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的范围是请求或方法范围。绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的引用放在任何类型的管理范围中,比如 Serlvet 架构中的 HttpSession。如果你现在正在使用一种 Web 框架,要考虑 SqlSession 放在一个和 HTTP 请求对象相似的范围中。换句话说,每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭

    • 工具类

      1. public class SqlSessionUtil {
      2. private static SqlSessionFactory sqlSessionFactory;
      3. static{
      4. try {
      5. InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
      6. SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
      7. sqlSessionFactory = sqlSessionFactoryBuilder.build(is);
      8. } catch (IOException e) {
      9. e.printStackTrace();
      10. }
      11. }
      12. public static SqlSession getSqlSession(){
      13. return sqlSessionFactory.openSession(true);
      14. }
      15. }

    六.缓存

    1.一级缓存

    • 一级缓存是SqlSession级别的,通过同一个SqlSession查询的数据会被缓存,下次查询相同的数据,就会从缓存中直接获取,不会从数据库重新访问,一级缓存是默认开启的
    • 一级缓存失效的四种情况:

      • 使用另一个SqlSession
      • 同一个SqlSession但是查询条件不同
      • 同一个SqlSession但是两次查询中间执行了任何一次增删改操作
      • 同一个SqlSession但是两次查询中间手动清空了缓存,手动清空缓存的方法是调用SqlSession的clearCache()方法

    2.二级缓存

    • 二级缓存是SqlSessionFactory级别,通过同一个SqlSessionFactory创建的SqlSession查询的结果会被缓存,此后若再次执行相同的查询语句,结果就会从缓存中获取
    • 二级缓存开启的条件:

      • 在核心配置文件中,设置全局配置属性cacheEnabled=”true”,默认为true,不需要设置
      • 在映射文件中设置标签
      • 二级缓存必须在SqlSession关闭或提交之后有效
      • 查询的数据所转换的实体类类型必须实现序列化的接口
    • 二级缓存失效的情况:两次查询之间执行了任意的增删改,会使一级和二级缓存同时失效
    • 可以设置的一些属性:

      • eviction属性:缓存回收策略,默认的是 LRU

        1. LRU(Least Recently Used) – 最近最少使用的:移除最长时间不被使用的对象
        2. FIFO(First in First out) – 先进先出:按对象进入缓存的顺序来移除它们
        3. SOFT – 软引用:移除基于垃圾回收器状态和软引用规则的对象
        4. WEAK – 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象
      • flushInterval属性:刷新间隔,单位是毫秒,默认情况下不设置也就是没有刷新间隔,缓存仅仅调用语句时刷新
      • size属性:引用数目,正整数代表缓存最多可以存储多少个对象,太大容易导致内存溢出
      • readOnly属性:只读, 取值是true/false

        1. true:只读缓存,会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,这提供了很重要的性能优势
        2. false:读写缓存,会返回缓存对象的拷贝(通过序列化),这会慢一些,但是安全,因此默认是false

    3.缓存的查询顺序

    • 先查询二级缓存,因为二级缓存中可能会有其他程序已经查出来的数据,可以拿来直接使用
    • 如果二级缓存没有命中,再查询一级缓存
    • 如果一级缓存也没有命中,则查询数据库
    • 注意SqlSession关闭之后,一级缓存中的数据才会写入二级缓存