脏数据的产生和避免

二级缓存虽然能提高应用效率,减轻数据库服务器的压力,但是如果使用不当,很容易产生脏数据。这些脏数据会在不知不觉中影响业务逻辑,影响应用的实效,所以我们需要了解在 MyBatis 缓存中脏数据是如何产生的,也要掌握避免脏数据的技巧。

MyBatis 的二级缓存是和命名空间绑定的,所以通常情况下每一个 Mapper 映射文件都拥有自己的二级缓存,不同 Mapper 的二级缓存互不影响。在常见的数据库操作中,多表联合查询非常常见,由于关系型数据库的设计,使得很多时候需要关联多个表才能获得想要的数据。在关联多表查询时肯定会将该查询放到某个命名空间下的映射文件中,这样一个多表的查询就会缓存在该命名空间的二级缓存中。涉及这些表的增、删、改操作通常不在一个映射文件中,它们的命名空间不同,因此当有数据变化时,多表查询的缓存未必会被清空,这种情况下就会产生脏数据。

下面通过测试来演示出现脏数据的情况。6.1.1节中,我们在 UserMapper 中创建了 selectUserAndRoleById 方法,该方法的 SQL 语句如下。

<select id="selectUserAndRoleById" resultType="cn.liaozh.mybatis2.ch6.query.model.SysUser">
    SELECT
        u.id,
        u.user_name   userName,
        u.user_password,
        userPassword,
        u.user_email  userEmail,
        u.user_info   userInfo,
        u.head_img    headImg,
        u.create_time createTime,
        r.id          "role.id",
        r.role_name   "role.roleName",
        r.enabled     "role.enabled",
        r.create_by   "role.createBy",
        r.create_time "role.createTime"
    FROM sys_user u
        INNER JOIN sys_user_role ur ON u.id = ur.user_id
        INNER JOIN sys_role r ON ur.role_id = r.id
    WHERE u.id = #{id}
</select>

这个 SQL 语句关联了两个表来查询用户对应的角色数据。给 UserMapper.xml 添加二级缓存配置,增加 <cache/> 元素,让 SysUser 对象实现 Serializable 接口。

在 RoleMapper 中,3.3节里增加了一个用注解实现的 updateById 方法,这个方法通过角色主键来更新角色的其他数据,现在通过这两个方法来演示二级缓存产生的脏数据,测试代码如下。

@Test
public void testDirtyData() {
    //获取 sqlSession
    SqlSession sqlSession = getSqlSession();
    try {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        SysUser user = userMapper.selectUserAndRoleById(1001L);
        Assert.assertEquals("普通用户", user.getRole().getRoleName());
        System.out.println("角色名:" + user.getRole().getRoleName());
    } finally {
        sqlSession.close();
    }
    //开始另一个新的 session
    sqlSession = getSqlSession();
    try {
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        SysRole role = roleMapper.selectById(2L);
        role.setRoleName("脏数据");
        roleMapper.updateById(role);
        //提交修改
        sqlSession.commit();
    } finally {
        //关闭当前的 sqlSession
        sqlSession.close();
    }
    System.out.println("开启新的 sqlSession");
    //开始另一个新的 session
    sqlSession = getSqlSession();
    try {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        RoleMapper roleMapper = sqlSession.getMapper(RoleMapper.class);
        SysUser user = userMapper.selectUserAndRoleById(1001L);
        SysRole role = roleMapper.selectById(2L);
        Assert.assertEquals("普通用户", user.getRole().getRoleName());
        Assert.assertEquals("脏数据", role.getRoleName());
        System.out.println("角色名:" + user.getRole().getRoleName());
        //还原数据
        role.setRoleName("普通用户");
        roleMapper.updateById(role);
        //提交修改
        sqlSession.commit();
    } finally {
        //关闭 sqlSession
        sqlSession.close();
    }
}

在这个测试中,一共有 3 个不同的 SqlSession。第一个 SqlSession 中获取了用户和关联的角色信息,第二个 SqlSession 中查询角色并修改了角色的信息,第三个 SqlSession 中查询用户和关联的角色信息。这时从缓存中直接取出数据,就出现了脏数据,因为角色名称已经修改,但是这里读取到的角色名称仍然是修改前的名字,因此出现了脏读。

该如何避免脏数据的出现呢?这时就需要用到参照缓存了。当某几个表可以作为一个业务整体时,通常是让几个会关联的 ER 表同时使用同一个二级缓存,这样就能解决脏数据问题。在上面这个例子中,将 UserMapper.xml 中的缓存配置修改如下。

<mapper namespace="tk.mybatis.simple.mapper.UserMapper">
    <cache-ref namespace="tk.mybatis.simple.mapper.RoleMapper" />
    <!--其它配置-->
</mapper>

修改为参照缓存后,再次执行测试,这时就会发现在第二次查询用户和关联角色信息时并没有使用二级缓存,而是重新从数据库获取了数据。虽然这样可以解决脏数据的问题,但是并不是所有的关联查询都可以这么解决,如果有几十个表甚至所有表都以不同的关联关系存在于各自的映射文件中时,使用参照缓存显然没有意义。