SpringBoot+MyBatis+Redis实现分布式缓存
作者:YYAugenstern
一、缓存介绍
1.1、概述
缓存是计算机内存中的一段数据(PS:内存中的数据具有读写快、断电立即消失的特点),合理地使用缓存能够提高网站的吞吐量和运行效率,减轻数据库的访问压力。那么哪些数据适合缓存呢?使用缓存时,一定是数据库中的数据极少发生改变,更多用于查询的情况,例如:省、市、区、县、村等数据。
1.2、本地缓存 vs 分布式缓存
- 本地缓存:存储在应用服务器内存中的数据称之为本地缓存(local cache);
- 分布式缓存:存储在当前应用服务器内存之外的数据称之为分布式缓存(distribute cache);
- 集群:将同一服务的多个节点放在一起,共同为系统提供服务的过程称之为集群(cluster);
- 分布式:由多个不同的服务集群共同对系统提供服务,那么这个系统就被称之为分布式系统(distribute system);
1.3、MyBatis默认的缓存策略
关于MyBatis的一级缓存、二级缓存请参考这篇文章,这里不再赘述。单机版的mybatis一级缓存默认是开启的,开启二级缓存也很简单,再mybatis的核心配置文件和xxxMapper.xml中分别添加如下配置即可激活MyBatis的二级缓存:
二级缓存也叫SqlSeesionFactory级别的缓存,其特点是所有会话共享。不管是一级缓存还是二级缓存,这些缓存都是本地缓存,适用于单机版。互联网发展的今天,生产级别的服务,不可能再使用单机版的了,基本都是微服务+分布式那一套,如果还使用MyBatis默认的缓存策略,显然是行不通的,为了解决这个问题,分布式缓存应运而生。
二、MyBatis中使用分布式缓存
2.1、基本思路
(1)自定义缓存实现Cache接口;
(2)在xxxMapper.xml中开启二级缓存时指明缓存的类型;
2.2、代码实战
2.2.1、项目概览
2.2.2、pom
<dependencies> <!-- springboot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency> <!-- 数据源 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- 工具 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.21</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> <dependency> <groupId>com.alibaba.fastjson2</groupId> <artifactId>fastjson2</artifactId> <version>2.0.25</version> </dependency> </dependencies>
2.2.3、yml
server: port: 9999 spring: redis: host: xxxx port: 6379 database: 0 password: 123456 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/20231018_redis?useSSL=false&useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: org.stat.entity.model configuration: map-underscore-to-camel-case: true logging: level: org: star: mapper: debug
2.2.4、MyRedisConfig
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 15:28 * @Description: */ @Configuration public class MyRedisConfig { /** * RedisTemplate k v 序列化 * @param connectionFactory * @return */ @Bean public RedisTemplate<Object, Object> redisTemplate(LettuceConnectionFactory connectionFactory) { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
2.2.5、MyRedisCache
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 15:30 * @Description: */ public class MyRedisCache implements Cache { /** * id为mapper中的namespace */ private final String id; private RedisTemplate getRedisTemplate() { RedisTemplate redisTemplate = (RedisTemplate) MyApplicationContextAware.getBean("redisTemplate"); return redisTemplate; } /** * 必须存在构造方法 * * @param id */ public MyRedisCache(String id) { System.out.println("RedisCache id============>" + id); this.id = id; } /** * 返回Cache的唯一标识 * * @return */ @Override public String getId() { return this.id; } /** * 往Redis缓存中存储数据 * @param key * @param value */ @Override public void putObject(Object key, Object value) { System.out.println("putObject key : " + key); System.out.println("putObject value : " + value); getRedisTemplate().opsForHash().put(Convert.toStr(id),key2MD5(Convert.toStr(key)),value); } /** * 从Redis缓存中取数据 * @param key * @return */ @Override public Object getObject(Object key) { System.out.println("getObject key : " + key); return getRedisTemplate().opsForHash().get(Convert.toStr(id),key2MD5(Convert.toStr(key))); } /** * 主要事项:这个方法为MyBatis的保留方法,默认没有实现,后续版本可能会实现 * @param key * @return */ @Override public Object removeObject(Object key) { System.out.println("removeObject key(根据指定Key删除缓存) : " + key); return null; } /** * 只要执行了增删改操作都会执行清空缓存的操作 */ @Override public void clear() { System.out.println("清空缓存"); getRedisTemplate().delete(Convert.toStr(id)); } /** * 计算缓存数量 * @return */ @Override public int getSize() { Long size = getRedisTemplate().opsForHash().size(Convert.toStr(id)); return size.intValue(); } /** * 将Key进行MD5加密 * @param key * @return */ private String key2MD5(String key) { return DigestUtils.md5DigestAsHex(key.getBytes(StandardCharsets.UTF_8)); } }
2.2.6、DepartmentDO
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 12:48 * @Description: */ @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) @ToString(callSuper = true) public class DepartmentDO implements Serializable { /** * 编号 */ private Integer id; /** * 部门名称 */ private String departmentName; }
2.2.7、DepartmentMapper
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 12:50 * @Description: */ public interface DepartmentMapper { /** * 查询所有部门 * @return */ List<DepartmentDO> listAllDepartment(); }
2.2.8、DepartmentMapper.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.star.mapper.DepartmentMapper"> <!-- 开启基于Redis的二级缓存 --> <cache type="org.star.cache.MyRedisCache"/> <select id="listAllDepartment" resultType="org.star.entity.model.DepartmentDO"> select id,department_name from department </select> </mapper>
2.2.9、DepartmentMapperTest
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 12:51 * @Description: */ @SpringBootTest public class DepartmentMapperTest { @Autowired private DepartmentMapper departmentMapper; @Test public void listAllDepartmentTest() { List<DepartmentDO> departments1 = departmentMapper.listAllDepartment(); System.out.println("departments1 = " + departments1); List<DepartmentDO> departments2 = departmentMapper.listAllDepartment(); System.out.println("departments2 = " + departments2); } }
2.3、存在的问题
2.3.1、问题说明
项目中如果某个业务涉及到的查询仅仅是单表查询,即类似上述的查询,这样使用分布式缓存一点问题没有,但是当有多张表关联查询时,将会出现问题。会出现什么问题呢?假设当前有两个持久化类,它们具有一对一的关联关系,例如员工 & 部门,从员工的角度看一个员工属于一个部门,部门表查询会缓存一条数据,员工表查询时也会缓存一条数据,下次再查询时将不会从DB中查询了,而是从缓存中取,那么当员工表中执行级联更新(增、删、改)时,将会清空员工对应的缓存 & 更新DB中员工表和部门表的数据,这个时候如果再次查询部门表中的数据,由于缓存中的数据还在,再次查询时直接从缓存中取数据了,导致查询到的数据(缓存中的数据)和实际数据库表中的数据不一致!案例演示(基于上边的案例,增加员工信息):
2.3.2、EmployeeDO
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 15:38 * @Description: */ @Data @AllArgsConstructor @NoArgsConstructor @Accessors(chain = true) @ToString(callSuper = true) public class EmployeeDO implements Serializable { /** * 员工编号 */ private Integer id; /** * 姓名 */ private String name; /** * 年龄 */ private Integer age; /** * 部门 */ private DepartmentDO department; }
2.3.3、EmployeeMapper
public interface EmployeeMapper { /** * 查询指定id员工的个人信息和部门信息 * @param id * @return */ EmployeeDO getDetail(Integer id); /** * 级联更新员工信息(更新员工信息 & 部门信息) * @param param */ void updateEmployeeCascade(EmployeeDO param); }
2.3.4、EmployeeMapper.xml
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.star.mapper.EmployeeMapper"> <!-- 开启基于Redis的分布式缓存 --> <cache type="org.star.cache.MyRedisCache"/> <resultMap id="employeeDetail" type="org.star.entity.model.EmployeeDO"> <id property="id" column="id"></id> <result property="name" column="name"></result> <result property="age" column="age"></result> <association property="department" javaType="org.star.entity.model.DepartmentDO"> <id property="id" column="id"></id> <result property="departmentName" column="department_name"></result> </association> </resultMap> <select id="getDetail" resultMap="employeeDetail"> select e.id, e.name,e.age, d.department_name from employee e, department d where e.department_id = d.id and e.id = #{id} </select> <delete id="updateEmployeeCascade"> update employee e left join department d on e.department_id = d.id <set> <if test="name != null and name != ''"> e.name = #{name}, </if> <if test="age != null"> e.age = #{age}, </if> <if test="department.departmentName != null and department.departmentName != ''"> d.department_name = #{department.departmentName} </if> </set> where e.id = #{id} </delete> </mapper>
2.3.5、EmployeeMapperTest
/** * @Author : 一叶浮萍归大海 * @Date: 2023/12/10 15:42 * @Description: */ @SpringBootTest public class EmployeeMapperTest { @Autowired private EmployeeMapper employeeMapper; @Autowired private DepartmentMapper departmentMapper; @Test public void listAllUserTest() { List<EmployeeDO> employeeDOS1 = employeeMapper.listAllEmployee(); System.out.println("employeeDOS1 = " + employeeDOS1); List<EmployeeDO> employeeDOS2 = employeeMapper.listAllEmployee(); System.out.println("employeeDOS2 = " + employeeDOS2); } @Test public void getUserByIdTest() { EmployeeDO employee1 = employeeMapper.getEmployeeById(2); System.out.println("employee1 ============> " + employee1); EmployeeDO employee2 = employeeMapper.getEmployeeById(2); System.out.println("employee2 ============> " + employee2); } @Test public void getDetailTest() { EmployeeDO employeeDO1 = employeeMapper.getDetail(2); System.out.println("employeeDO1 = " + employeeDO1); EmployeeDO employeeDO2 = employeeMapper.getDetail(2); System.out.println("employeeDO2 = " + employeeDO2); } @Test public void relationShipTest() { EmployeeDO employeeDO = employeeMapper.getDetail(2); System.out.println("employeeDO = " + employeeDO); List<DepartmentDO> departmentDOS = departmentMapper.listAllDepartment(); System.out.println("departmentDOS = " + departmentDOS); } @Test public void updateEmployeeCascadeTest() { EmployeeDO employeeDO = new EmployeeDO() .setId(2) .setName("刘亦菲") .setAge(18) .setDepartment( new DepartmentDO() .setId(2) .setDepartmentName("市场部") ); employeeMapper.updateEmployee(employeeDO); } }
2.3.6、测试
(1)执行EmployeeMapperTest #getDetailTest
(2)执行 DepartmentMapperTest #listAllDepartmentTest
(3)级联更新 EmployeeMapperTest #updateEmployeeCascadeTest,将id为2的部门名称改为市场部,执行完此操作后,redis中员工相关的缓存将被清空;
(4)再次执行DepartmentMapperTest #listAllDepartmentTest
结果分析:查询到的数据和数据库中的数据不符。
原因:
具有级联关系的查询,当执行级联更新(增、删、改)时将会触发清空redis缓存,而清空缓存是按照mapper中配置的namespace进行删除的,导致被关联的那一方即使DB中的数据被更新了,redis中对应的缓存也不会被清空。
2.3.7、解决方案
在级联更新的xxxMapper.xml中使用<cache-ref type="xxx"/>进行级联清空缓存,如下:
<cache-ref namespace="org.star.mapper.DepartmentMapper"/>
到此这篇关于SpringBoot+MyBatis+Redis实现分布式缓存的文章就介绍到这了,更多相关SpringBoot MyBatis Redis分布式缓存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!