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