系列九、SpringBoot + MyBatis + Redis实现分布式缓存

news2024/11/29 20:31:52

一、缓存介绍

1.1、概述

        缓存是计算机内存中的一段数据(PS:内存中的数据具有读写快、断电立即消失的特点),合理地使用缓存能够提高网站的吞吐量和运行效率,减轻数据库的访问压力。那么哪些数据适合缓存呢?使用缓存时,一定是数据库中的数据极少发生改变,更多用于查询的情况,例如:省、市、区、县、村等数据。

1.2、本地缓存 vs 分布式缓存

本地缓存:存储在应用服务器内存中的数据称之为本地缓存(local cache); 

分布式缓存:存储在当前应用服务器内存之外的数据称之为分布式缓存(distribute cache);

集群:将同一服务的多个节点放在一起,共同为系统提供服务的过程称之为集群(cluster);

分布式:由多个不同的服务集群共同对系统提供服务,那么这个系统就被称之为分布式系统(distribute system);

1.3、MyBatis默认的缓存策略

        关于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"/>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1299847.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++笔记之通过静态类成员变量的方式在不同的类之间传递参数

C笔记之通过静态类成员变量的方式在不同的类之间传递参数 code review! 在C中&#xff0c;可以使用静态类成员变量作为一种在不同类之间传递参数的方式。静态类成员变量是类的所有对象之间共享的变量&#xff0c;它们存在于类的内部&#xff0c;但不属于任何特定的类对象。 …

qt:QMessageBox的常见用法

头文件&#xff1a;#include <QMessageBox> Infomation消息对话框 初始化格式&#xff1a; QMessageBox * msgBox new QMessageBox(QMessageBox::Information, "我是标题", "我是提示文字", 按钮); 按钮可以是以下取值&#xff0c;会在按键上显示…

Python开发运维:Python调用K8S API实现资源管理

目录 一、实验 1.Python操作K8S API获取资源 2.Python操作K8S API创建deployment资源 3.Python操作K8S API删除k8s资源 4.Python操作K8S API修改k8s资源 5.Python操作K8S API查看k8s资源 二、问题 1.Windows11安装kubernetes报错 2.Python通过调用哪些方法实现Pod和De…

变容二极管测量

测量变量二极管一般有两种方法&#xff1a;一是搭建偏置电路&#xff0c;用LCR电桥测量电容&#xff1b;二是搭建一个VCO&#xff0c;通过测量频率&#xff0c;简接测量变容二极管的电容值。 正好手里有许老师LCR&#xff0c;搭建一个简单的测试电路即可&#xff0c;电路图如下…

复旦量化多策略公开课总结

《掘金之心公众号&#xff1a;gnu_isnot_unix》前Citadel现自营交易与量化管理&#xff0c;分享热点&#xff0c;主观&#xff0c;量化交易内容。活在当下&#xff0c;终身学习 - 给在职却对未来始终迷茫的人的公众号。借此想告诉不断努力&#xff0c;对生活充满热情的读者们&a…

基于ssm在线云音乐系统的设计与实现论文

摘 要 随着移动互联网时代的发展&#xff0c;网络的使用越来越普及&#xff0c;用户在获取和存储信息方面也会有激动人心的时刻。音乐也将慢慢融入人们的生活中。影响和改变我们的生活。随着当今各种流行音乐的流行&#xff0c;人们在日常生活中经常会用到的就是在线云音乐系统…

洛谷P1450 硬币购物

传送门&#xff1a; P1450 [HAOI2008] 硬币购物 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P1450 题干&#xff1a; 题目描述 共有 4 种硬币。面值分别为 c1​,c2​,c3​,c4​。 某人去商店买东西&#xff0c;去了 n 次&#xff0c;对于…

【Angular开发】Angular在2023年之前不是很好

做一个简单介绍&#xff0c;年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【架构师酒馆】…

eclipse中maven的配置

Maven下载地址&#xff1a;https://maven.apache.org/download.cgi 下载完成以后解压到非中文目录&#xff0c;建议放一个比较大的盘符下&#xff0c;因为Maven会一直从网上更新各种库存放在这个目录下&#xff0c;慢慢的会变得很大。 Maven环境变量配置 创建环境变量 在桌…

Diary22-全网最全的CSS3.0讲解

CSS学习 1.认识CSS 1.1什么是CSS CSS&#xff1a;Cascading Style Sheet——层叠级联样式表 CSS&#xff1a;表现&#xff08;美化网页&#xff09; 字体&#xff1b;颜色&#xff1b;边距&#xff1b;高度&#xff1b;宽度&#xff1b;背景图片&#xff1b;网页定位&…

再见了Future,图解JDK21虚拟线程的结构化并发

Java为我们提供了许多启动线程和管理线程的方法。在本文中&#xff0c;我们将介绍一些在Java中进行并发编程的选项。我们将介绍结构化并发的概念&#xff0c;然后讨论Java 21中一组预览类——它使将任务拆分为子任务、收集结果并对其进行操作变得非常容易&#xff0c;而且不会不…

【图片版】计算机组成原理考前复习题【第2章 运算方法和运算器-1】

目录 前言 考前复习题&#xff08;必记&#xff09; 结尾 前言 在计算机组成原理的学习过程中&#xff0c;我们深入探索了计算机系统概述这一重要领域。计算机系统作为现代科技的核心&#xff0c;是整个计算机科学的基石。我们将学到的知识与理论转化为了能够解决现实问题的…

3DMAX关于显示驱动问题的解决方法大全

3DMAX与显卡驱动有关的问题主要有以下几种情况&#xff1a; 1.3DMAX启动弹出这样的界面&#xff1a; 2.主工具栏按钮不显示&#xff0c;或者鼠标移上去才显示&#xff08;刷新问题&#xff09;。 3&#xff0e;视口菜单不显示或显示不全。 问题分析&#xff1a; 首先&#x…

AspNetCore 中使用 Knife4jUI 更加友好的Swagger界面

&#x1f680;介绍 aspnetcore.knife4j是一个基于.NET Core平台的Swagger UI库&#xff0c;它提供了API文档的生成和管理功能。这个库的前身是swagger-bootstrap-ui&#xff0c;在Java项目中广泛使用&#xff0c;由于其优秀的界面和易用性被许多开发者所推崇。现在&#xff0c…

一文学会使用 PyInstaller 将 Python 脚本打包为 .exe 可执行文件

文章目录 前言PyInstaller特点跨平台支持自动依赖项处理单文件发布支持图形用户界面&#xff08;GUI&#xff09;和命令行界面&#xff08;CLI&#xff09;应用支持多种打包选项 基本用法常用参数其它参数 版本 & 环境实现步骤安装 PyInstaller创建 Python 脚本使用 PyInst…

C++学习笔记之五(String类)

C 前言getlinelength, sizec_strappend, inserterasefindsubstrisspace, isdigit 前言 C是兼容C语言的&#xff0c;所以C的字符串自然继承C语言的一切字符串&#xff0c;但它也衍生出属于自己的字符串类&#xff0c;即String类。String更像是一个容器&#xff0c;但它与容器还…

HJ103 Redraiment的走法

题目&#xff1a; HJ103 Redraiment的走法 题解&#xff1a; dfs 暴力搜索 枚举数组元素&#xff0c;作为起点如果后续节点大于当前节点&#xff0c;继续向后搜索记录每个起点的结果&#xff0c;求出最大值 public int getLongestSub(int[] arr) {int max 0;for (int i 0…

MySQL - 表达式With as 语句的使用及练习

目录 8.1 WITH AS 的含义 8.2 WITH AS语法的基本结构如下&#xff1a; 8.3 练习题1 8.4 牛客练习题 8.1 WITH AS 的含义 WITH AS 语法是MySQL中的一种临时结果集&#xff0c;它可以在SELECT、INSERT、UPDATE或DELETE语句中使用。通过使用WITH AS语句&#xff0c;可以将一个查…

flstudio21.3完整高级版怎么下载?有哪些新功能

flstudio高级版是一款适用于广泛领域的音频编辑软件。它支持多通道混音器和VST插件&#xff0c;包括数百种乐器和效果插件。它还为您提供了一个乐谱编辑器&#xff0c;需要对不同乐器的节奏进行必要的编辑。Flstudio具有许多内置电子合成声音&#xff0c;可提供更广泛的电子声音…

运维06:监控

监控生命周期 1.服务器上架到机柜2.基础设施监控 服务器温度&#xff0c;风扇转速 ipmitool命令&#xff0c;只能用在物理机上 存储的监控&#xff08;df, fdisk, iotop&#xff09; cpu&#xff08;lscpu, uptime, top, htop, glances&#xff09; 内存情况&#xff08;free&…