SpringBoot+MyBatis+Redis實現(xiàn)分布式緩存
一、緩存介紹
1.1、概述
緩存是計算機(jī)內(nèi)存中的一段數(shù)據(jù)(PS:內(nèi)存中的數(shù)據(jù)具有讀寫快、斷電立即消失的特點),合理地使用緩存能夠提高網(wǎng)站的吞吐量和運行效率,減輕數(shù)據(jù)庫的訪問壓力。那么哪些數(shù)據(jù)適合緩存呢?使用緩存時,一定是數(shù)據(jù)庫中的數(shù)據(jù)極少發(fā)生改變,更多用于查詢的情況,例如:省、市、區(qū)、縣、村等數(shù)據(jù)。
1.2、本地緩存 vs 分布式緩存
- 本地緩存:存儲在應(yīng)用服務(wù)器內(nèi)存中的數(shù)據(jù)稱之為本地緩存(local cache);
- 分布式緩存:存儲在當(dāng)前應(yīng)用服務(wù)器內(nèi)存之外的數(shù)據(jù)稱之為分布式緩存(distribute cache);
- 集群:將同一服務(wù)的多個節(jié)點放在一起,共同為系統(tǒng)提供服務(wù)的過程稱之為集群(cluster);
- 分布式:由多個不同的服務(wù)集群共同對系統(tǒng)提供服務(wù),那么這個系統(tǒng)就被稱之為分布式系統(tǒng)(distribute system);
1.3、MyBatis默認(rèn)的緩存策略
關(guān)于MyBatis的一級緩存、二級緩存請參考這篇文章,這里不再贅述。單機(jī)版的mybatis一級緩存默認(rèn)是開啟的,開啟二級緩存也很簡單,再mybatis的核心配置文件和xxxMapper.xml中分別添加如下配置即可激活MyBatis的二級緩存:
二級緩存也叫SqlSeesionFactory級別的緩存,其特點是所有會話共享。不管是一級緩存還是二級緩存,這些緩存都是本地緩存,適用于單機(jī)版?;ヂ?lián)網(wǎng)發(fā)展的今天,生產(chǎn)級別的服務(wù),不可能再使用單機(jī)版的了,基本都是微服務(wù)+分布式那一套,如果還使用MyBatis默認(rèn)的緩存策略,顯然是行不通的,為了解決這個問題,分布式緩存應(yīng)運而生。
二、MyBatis中使用分布式緩存
2.1、基本思路
(1)自定義緩存實現(xiàn)Cache接口;
(2)在xxxMapper.xml中開啟二級緩存時指明緩存的類型;
2.2、代碼實戰(zhàn)
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> <!-- 數(shù)據(jù)源 --> <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 : 一葉浮萍?xì)w大海 * @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 : 一葉浮萍?xì)w大海 * @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; } /** * 必須存在構(gòu)造方法 * * @param id */ public MyRedisCache(String id) { System.out.println("RedisCache id============>" + id); this.id = id; } /** * 返回Cache的唯一標(biāo)識 * * @return */ @Override public String getId() { return this.id; } /** * 往Redis緩存中存儲數(shù)據(jù) * @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緩存中取數(shù)據(jù) * @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的保留方法,默認(rèn)沒有實現(xiàn),后續(xù)版本可能會實現(xiàn) * @param key * @return */ @Override public Object removeObject(Object key) { System.out.println("removeObject key(根據(jù)指定Key刪除緩存) : " + key); return null; } /** * 只要執(zhí)行了增刪改操作都會執(zhí)行清空緩存的操作 */ @Override public void clear() { System.out.println("清空緩存"); getRedisTemplate().delete(Convert.toStr(id)); } /** * 計算緩存數(shù)量 * @return */ @Override public int getSize() { Long size = getRedisTemplate().opsForHash().size(Convert.toStr(id)); return size.intValue(); } /** * 將Key進(jìn)行MD5加密 * @param key * @return */ private String key2MD5(String key) { return DigestUtils.md5DigestAsHex(key.getBytes(StandardCharsets.UTF_8)); } }
2.2.6、DepartmentDO
/** * @Author : 一葉浮萍?xì)w大海 * @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 : 一葉浮萍?xì)w大海 * @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 : 一葉浮萍?xì)w大海 * @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、問題說明
項目中如果某個業(yè)務(wù)涉及到的查詢僅僅是單表查詢,即類似上述的查詢,這樣使用分布式緩存一點問題沒有,但是當(dāng)有多張表關(guān)聯(lián)查詢時,將會出現(xiàn)問題。會出現(xiàn)什么問題呢?假設(shè)當(dāng)前有兩個持久化類,它們具有一對一的關(guān)聯(lián)關(guān)系,例如員工 & 部門,從員工的角度看一個員工屬于一個部門,部門表查詢會緩存一條數(shù)據(jù),員工表查詢時也會緩存一條數(shù)據(jù),下次再查詢時將不會從DB中查詢了,而是從緩存中取,那么當(dāng)員工表中執(zhí)行級聯(lián)更新(增、刪、改)時,將會清空員工對應(yīng)的緩存 & 更新DB中員工表和部門表的數(shù)據(jù),這個時候如果再次查詢部門表中的數(shù)據(jù),由于緩存中的數(shù)據(jù)還在,再次查詢時直接從緩存中取數(shù)據(jù)了,導(dǎo)致查詢到的數(shù)據(jù)(緩存中的數(shù)據(jù))和實際數(shù)據(jù)庫表中的數(shù)據(jù)不一致!案例演示(基于上邊的案例,增加員工信息):
2.3.2、EmployeeDO
/** * @Author : 一葉浮萍?xì)w大海 * @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); /** * 級聯(lián)更新員工信息(更新員工信息 & 部門信息) * @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 : 一葉浮萍?xì)w大海 * @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)執(zhí)行EmployeeMapperTest #getDetailTest
(2)執(zhí)行 DepartmentMapperTest #listAllDepartmentTest
(3)級聯(lián)更新 EmployeeMapperTest #updateEmployeeCascadeTest,將id為2的部門名稱改為市場部,執(zhí)行完此操作后,redis中員工相關(guān)的緩存將被清空;
(4)再次執(zhí)行DepartmentMapperTest #listAllDepartmentTest
結(jié)果分析:查詢到的數(shù)據(jù)和數(shù)據(jù)庫中的數(shù)據(jù)不符。
原因:
具有級聯(lián)關(guān)系的查詢,當(dāng)執(zhí)行級聯(lián)更新(增、刪、改)時將會觸發(fā)清空redis緩存,而清空緩存是按照mapper中配置的namespace進(jìn)行刪除的,導(dǎo)致被關(guān)聯(lián)的那一方即使DB中的數(shù)據(jù)被更新了,redis中對應(yīng)的緩存也不會被清空。
2.3.7、解決方案
在級聯(lián)更新的xxxMapper.xml中使用<cache-ref type="xxx"/>進(jìn)行級聯(lián)清空緩存,如下:
<cache-ref namespace="org.star.mapper.DepartmentMapper"/>
到此這篇關(guān)于SpringBoot+MyBatis+Redis實現(xiàn)分布式緩存的文章就介紹到這了,更多相關(guān)SpringBoot MyBatis Redis分布式緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springsecurity第三方授權(quán)認(rèn)證的項目實踐
Spring security 是一個強(qiáng)大的和高度可定制的身份驗證和訪問控制框架,本文主要介紹了springsecurity第三方授權(quán)認(rèn)證的項目實踐,具有一定的參考價值,感興趣可以了解一下2023-08-08Mybatis?List列表In查詢實現(xiàn)的注意事項說明
這篇文章主要介紹了Mybatis?List列表In查詢實現(xiàn)的注意事項說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02spring-boot 多線程并發(fā)定時任務(wù)的解決方案
這篇文章主要介紹了spring-boot 多線程并發(fā)定時任務(wù)的解決方案,需要的朋友可以參考下2019-08-08Java Yml格式轉(zhuǎn)換為Properties問題
本文介紹了作者編寫一個Java工具類來解決在線YAML到Properties轉(zhuǎn)換時屬性內(nèi)容遺漏的問題,通過遍歷YAML文件的樹結(jié)構(gòu),作者成功實現(xiàn)了屬性的完整轉(zhuǎn)換,總結(jié)指出,該工具類適用于多種數(shù)據(jù)類型,并且代碼簡潔易懂2024-12-12解決@PostConstruct注解導(dǎo)致的程序無法啟動(@PostConstruct的執(zhí)行)
這篇文章主要介紹了解決@PostConstruct注解導(dǎo)致的程序無法啟動(@PostConstruct的執(zhí)行)問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01