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-08
Mybatis?List列表In查詢實現(xiàn)的注意事項說明
這篇文章主要介紹了Mybatis?List列表In查詢實現(xiàn)的注意事項說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
spring-boot 多線程并發(fā)定時任務(wù)的解決方案
這篇文章主要介紹了spring-boot 多線程并發(fā)定時任務(wù)的解決方案,需要的朋友可以參考下2019-08-08
Java 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

