Redis实战—黑马点评(二)缓存篇

news2024/11/16 18:56:27

Redis实战—黑马点评(二)缓存篇

目录

  • Redis实战—黑马点评(二)缓存篇
    • 1. 什么是缓存
      • 1.1 缓存的作用和成本
    • 2. 添加 Redis 缓存
    • 3. 缓存更新策略
      • 3.1 三种更新策略
        • 3.1.1 主动更新策略
    • 4. 缓存穿透
      • 4.1 常见两种解决办法
        • 4.1.1 缓存空值
        • 4.1.2 布隆过滤器
      • 4.2 小结
    • 5. 缓存雪崩
    • 6. 缓存击穿
      • 6.1 常见解决方案两种
        • 6.1.1 互斥锁
        • 6.1.2 逻辑过期
        • 6.1.3 方案对比
      • 6.2 小收获
    • 7. 缓存工具类封装
      • 7.1 代码实现
        • 问题:刚才的代码中,锁的key不应该在代码中指定,而是作为参数传入。
      • 7.2 小收获
        • 7.2.1 时间处理
        • 7.2.2 泛型使用
        • 7.2.3 Function对象

在这里插入图片描述

1. 什么是缓存

缓存即为数据交换的缓冲区,是数据临时存储的地方,读写性能高

在这里插入图片描述

1.1 缓存的作用和成本

在这里插入图片描述

简单的说,用缓存好处多,可以提升程序性能,缺点就是麻烦。

2. 添加 Redis 缓存

不用缓存的话,我们的程序获取数据是从数据库:

在这里插入图片描述

用了缓存之后,减少了数据库的压力:

在这里插入图片描述

原本需要从数据库中查的数据现在只需要去缓存中查了(第一次除外)。

案例:添加商户缓存

在这里插入图片描述

具体代码就不写了,这个流程还存在缓存穿透的问题,后续会说明。

这里收获了一个小编码技巧,在软件工程中有“单一出口原则”的说法,在方法中就只有一个return,但有时候用多个return会大幅简洁代码,省掉大量的else。

3. 缓存更新策略

数据存到缓存后,会有一个数据一致性的人问题:

假如数据库更新了,用户还是从缓存查数据,那么查到的就是缓存中的旧数据了。

所以需要有一种更行缓存的机制。

3.1 三种更新策略

  1. 内存淘汰

    Redis自带的机制,当内存不足时自动淘汰部分数据。下次查询时更新缓存。

  2. 超时剔除

    给数据添加TTL值,到期自动删除。下次查询时更新。

  3. 主动更新

    主动编写逻辑,在数据改变时,更行缓存。

在这里插入图片描述

3.1.1 主动更新策略

在这里插入图片描述

第一种就是主动编码,在数据库更新时更新缓存(编码多,麻烦)。

第二种就是将缓存和数据库整合为一个服务,由该服务来保证一致性(编码少,缺点就是这个服务质量不好保证,不好维护)。

第三种就是只操作缓存,由其他服务来将缓存同步到数据库,保证最终一致性。(完全不用关心数据库,但是当缓存没有同步时宕机了,数据将不会同步到数据库)。

第一二种都是强一致性,第三种是保证最终一致性。

方案一强一致且可控,用的较多。

在开始编码前,还有三个问题需要考虑:

  1. 更新数据库时,缓存是更新还是删除?

    更新:每次更新数据库都更新缓存

    删除:更新时删除缓存,查询时更新缓存

    一般是选择删除的方案,因为不是每次更新都要查询的,不查询,也就省去了添加缓存的步骤,避免浪费内存,性能。

  2. 如何保证同时成功或失败?

    单体系统:将缓存和数据库操作放在同一个事务中。

    分布式系统:利用TCC等分布式事务方案。

  3. 先操作缓存还是数据库?

    本质上是线程安全问题

    看两个例子,初始状态数据库,缓存都为10

    在这里插入图片描述

    先删除缓存,在数据库更新这个耗时操作完成之前,其他线程将未更新前的数据更新到缓存,然后原来的线程再更新数据库,造成数据的不一致。

    例2:在这里插入图片描述

    假设缓存在线程一查询时恰好失效(或没有该缓存),将会查询数据库,此时数据库是老数据,在将该老数据写入缓存时,线程二进行数据库更新和缓存删除操作,最后线程一写入老数据,造成数据不一致。

    **例二这种情况发生概率很小,因为写缓存的操作是微秒级的,短时间内不太可能完成数据库更新和缓存删除的操作。例一的情况发生的概率还是比较大的。**所以一般先操作数据库再删缓存并且给数据加上一个TTL过期时间,最大限度保证线程安全。

在这里插入图片描述

4. 缓存穿透

缓存穿透是指,客户端的请求数据在数据库和缓存都不存在,但是这些请求永远会打向数据库,给数据库造成压力。

4.1 常见两种解决办法

在这里插入图片描述

4.1.1 缓存空值

即使数据库不存在,我们也在redis中缓存一个空。

优点:简单,易维护。

缺点:

  • 额外的内存消耗(可加一个TTL缓解)
  • 可能造成短期的数据不一致(当缓存空值时,该数据正好被插入数据库,客户端请求缓存获得空)

4.1.2 布隆过滤器

用算法实现的记录所请求的数据是否存在的过滤器。底层像是BitMap。当数据库中不存在该数据时,会拒绝请求,存在时才放行。

存在误判可能,有可能出现数据不存在布隆过滤器却认为存在的情况,但是布隆过滤器若是认为数据不存在,那就一定不存在。

优点就是不占用额外内存,缺点就是实现复杂,存在误判可能。

使用方案一,简单高效

所以商户缓存的流程将被优化为:

在这里插入图片描述

4.2 小结

在这里插入图片描述

5. 缓存雪崩

缓存雪崩是指,在同一时间大量的key同时失效或redis服务宕机,导致大量请求到达数据库,给数据库造成过大压力。

解决方案比较常见的有:

  1. 给不同的key的TTL添加随机值

    不在同一时间过期,添加几分钟随机值(根据业务)。

  2. Redis集群,提高可用性

    避免redis服务全部挂掉。

  3. 缓存业务降级限流策略

    亡羊补牢,数据库再崩了就是更大的损失。

  4. 添加多级缓存

    浏览器缓存,nginx缓存等,即使redis崩了,还有其他缓存可查。

6. 缓存击穿

也被称为热点key问题,即 一个被高并发访问并且缓存重建业务复杂的key突然失效,大量请求给数据库带来巨大压力。

在这里插入图片描述

在缓存重建期间大量请求到达数据库。

6.1 常见解决方案两种

在这里插入图片描述

6.1.1 互斥锁

当缓存重建业务开始时,其他线程等待并重试,保证没有多余的请求到达数据库。该互斥锁的简单时间可以用setnx命令来实现。(在后续秒杀篇中会实现该分布式锁)

在这里插入图片描述

6.1.2 逻辑过期

用一个字段来保存过期时间,查询该缓存时判断缓存是否过期,如果过期则返回旧数据,并让另一个线程异步地重建缓存,重建缓存时需要加锁,其他线程来获取锁失败时,返回旧数据。

在这里插入图片描述

6.1.3 方案对比

在这里插入图片描述

方案一牺牲性能保证一致性,方案二则反之,根据业务需求选择。

6.2 小收获

若使用方案二,需要添加一个字段,如何添加呢?

在数据库添加是最笨的方式并且不合理。

在往常,我会新建一个DTO,然后添加该字段,使用该DTO对象进行数据传输。

在该课程中,我学会了如下方案:

  1. 使用MP的注解

    在这里插入图片描述

    但是这样就破坏了该对象的结构

  2. 创建一个新的类继承该类,然后添加新字段。(DTO)

  3. 创建一个新类,添加该字段,让原类继承该类。(和一一样,破坏了对象的结构)

  4. 创建一个新类,使用泛型,该类包含新的字段和一个泛型对象。(优雅)

    @Data
    public class RedisData<T> {
        private LocalDateTime expireTime;
        private T data;
    }
    

7. 缓存工具类封装

7.1 代码实现

在这里插入图片描述

简单说就是:1、3配合解决缓存穿透,2、4配合解决缓存击穿,四个方法,两个设置两个读取。

方法一:

public <R> void set(String key, R value, Long time, TimeUnit unit) {
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
}

方法二:

public <R> void setWithExpire(String key, R value, Long time, TimeUnit unit) {
    RedisData<R> redisData = new RedisData<>();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    // 写入redis
    stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
}

方法三:

public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Type type, boolean ignoreError, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    R r;
    String key = keyPrefix + id;
    // 1. 从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否存在
    if (StrUtil.isNotBlank(json)) {
        // 3.1 存在
        r = JSONUtil.toBean(json, type, ignoreError);
        return r;
    }
    // 3.2 不存在
    // 判断是否是空值(防止缓存穿透)
    if (json != null) {
        return null;
    }
    // 从数据库查询
    r = dbFallback.apply(id);

    // 4. 返回
    if (r == null) {
        // 不存在 返回错误 将空值写入redis
        stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
        return null;
    }
    // 存在 写入redis
    this.set(key, r, time, unit);
    return r;
}

方法四:

public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Type type, boolean ignoreError, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
    R r;
    String key = keyPrefix + id;
    // 1. 从redis查询商铺缓存
    String json = stringRedisTemplate.opsForValue().get(key);
    // 2. 判断是否命中
    if (StrUtil.isBlank(json)) {
        // 3.1 未命中
        return null;
    }
    // 3.2 命中
    RedisData<R> redisData = JSONUtil.toBean(json, new TypeReference<RedisData<R>>() {}.getType(), ignoreError);
    r = JSONUtil.toBean(JSONUtil.toJsonStr(redisData.getData()), type, false);
    if (LocalDateTime.now().isBefore(redisData.getExpireTime())) {
        // 4. 判断是否过期
        // 4.1 未过期 返回
        return r;
    }
    // 4.2 过期
    // 5. 缓存重建
    // 5.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    if (tryLock(lockKey)) {
        // 5.2 成功 开启另一线程重建
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                // 查数据库
                R newR = dbFallback.apply(id);
                // 更新缓存
                setWithExpire(key, newR, time, unit);
                // 模拟业务延时
                Thread.sleep(200);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                unLock(lockKey);
            }
        });
    }
    // 5.3 失败
    // 无论是否成功都返回旧数据
    return r;
}

锁的简单实现

private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
   	// 当flag为null时,若直接返回将会拆箱失败,报错,所以使用hutool的BooleanUtil工具保证拆箱成功(不用也可以,用Boolean.TRUE.equals方法)
    return BooleanUtil.isTrue(flag);
}

private void unLock(String key) {
    stringRedisTemplate.delete(key);
}

问题:刚才的代码中,锁的key不应该在代码中指定,而是作为参数传入。

7.2 小收获

收获到了很多编码小技巧:

7.2.1 时间处理

  1. 判断某个值是否过期

    if (LocalDateTime.now().isBefore(redisData.getExpireTime())) {
    
    }
    

    可以用LocalDateTime的isBefore和isAfter方法

  2. 续期

    使用LocalDateTime实例中的plusxxx方法,例如:

    LocalDateTime time = LocalDateTime.now().plusSeconds(2000);
    

    即可在当前时间的基础上添加2000秒得到一个新时间

  3. 单位转换

    刚刚续期的代码中plusSeconds,不一定每次都是Seconds,很不方便,那如何同一代码呢?

    使用TimeUtil中的方法:

    System.out.println(TimeUnit.MINUTES.toSeconds(1)); // 60
    System.out.println(TimeUnit.SECONDS.toMinutes(60)); // 1
    

    所以续期的代码可以统一为:

    public void setWithExpire(Long time, TimeUnit unit) {
        LocalDateTime.now().plusSeconds(unit.toSeconds(time));
    }
    

7.2.2 泛型使用

泛型类:

Class<T> IData { 
	private T data;
}

泛型方法:

<R> void test(R r) {
    // 定义泛型后,一般会在参数列表中接受泛型来指定泛型类型。
} 

泛型的JSON转换:

将JSON字符串转换为含有自定义泛型的对象。

假设有如下类:

@Data
@AllArgsConstructor
@NoArgsConstructor
class IData<T> {
    private Long id;
    private T data;
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class User {
    private String name;
    private int age;
}

一般情况下,因为有类型擦除,我们不会直接.class,我们都是使用TypeRefrence的方式:

/**
* 用哪种json解析工具用法都差不多 都是传type、class这些
*/
<R> R json2Obj3(String json, TypeReference<R> type) throws JsonProcessingException {
    R data = JSONUtil.toBean(json, type, false);
    return data;
}

@Test
void testParse() throws JsonProcessingException {
    // 创建json字符串
    User user = new User("张三", 18);
    IData<User> iData = new IData<>(1L, user);
    String json = JSONUtil.toJsonStr(iData);
    // 解析
    IData<User> data = json2Obj3(json, new TypeReference<IData<User>>() {
    });
    // 能输出name才算成功
    System.out.println(data.getData().getName());
}

若是传class,那么就指定泛型的class对象:

<R> IData<R> json2Obj2(String json, Class<R> type) throws JsonProcessingException {
    IData<R> data = JSONUtil.toBean(json, new TypeReference<IData<R>>() {}, false);
    // 这个时候就需要转换两次了 因为在该方法中R就是R类型而不是User类型
    data.setData(JSONUtil.toBean(JSONUtil.toJsonStr(data.getData()), type));
    return data;
}

@Test
void test1() throws JsonProcessingException {
    // 创建json字符串
    User user = new User("张三", 18);
    IData<User> iData = new IData<>(1L, user);
    String json = JSONUtil.toJsonStr(iData);
    // 解析
    IData<User> data = json2Obj2(json, User.class);
    // 能输出name才算成功
    System.out.println(data.getData().getName());
}

类型探究:

<R, T> void testType(TypeReference<R> type1, Class<T> type2) {
    System.out.println(type1);
    System.out.println(type2);
    System.out.println(new TypeReference<R>() {
    });
    System.out.println(new TypeReference<T>() {
    });
}

@Test
void testType1() {
    testType(new TypeReference<User>() {
    }, User.class);
}

输出:

在这里插入图片描述

JSON转换小结:在泛型方法内,R就是R对象,不是其他的什么对象,只有将具体的类型传进来才能解析。

7.2.3 Function对象

Java8的新特性,可以像js一样丝滑:

void testFunc(String param, Function<String, String> function) {
    String result = function.apply(param);
    System.out.println("result: " + result);
}

@Test
void testFunc1() {
    testFunc("hello", s -> {
        System.out.println("in func: " + s + " world!");
        return s + " result";
    });
}

黑马点评中,Redis工具类的封装,当方法中需要查询数据库,但是各个业务的查询逻辑可能不同,于是就在参数列表中加上了Function对象用于传入该逻辑。当然也可以不用Function,直接接受结果,这个结果在方法之外获得。

Function还有很多用法:

function源码

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

andThen,compose返回值都是Function,可以链式编程,自由组合各种函数。

ion源码

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

andThen,compose返回值都是Function,可以链式编程,自由组合各种函数。

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

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

相关文章

数仓理论【范式】【维度建模】

数仓理论 1 范式理论 1.1 范式概念 数据建模要遵循一定的规则&#xff0c;在关系建模中&#xff0c;这种规则就是范式 采用范式结构&#xff0c;可以有效的降低数据的冗余性 范式在获取数据时&#xff0c;需要通过join拼接出数据 范式有第一范式(1NF)&#xff0c;第二范式…

双击-jar包无法运行解决方法

我自己是通过探索出来的方法解决的&#xff0c;网上的方法适合普通问题 网络流传方法 那种-jar和run.bat的就是曲解了问题意思&#xff0c;问题不是如何运行&#xff0c;而是如何双击jar包就可以直接运行。 普通小问题就是修改注册表&#xff0c;将java路径写进去后面加个 %1…

基于JDBC框架的事务管理

事务: Transaction, 是数据库中的一种能够保证多个写操作要么全部成功, 要么全部失败的机制在基于Spring JDBC的数据库编程中, 在业务方法上添加Transactional注解, 即可使得这个业务方法是事务性的举例, 一个银行转账操作, 转账时需要执行的sql语句大致是:UPDATE 存款表 SET 余…

前端必学的CSS制作Switch动画开关按钮演示

目录 前言 CSS 制作的 Switch 动画开关按钮 1.Html构建 2.CSS编写 3.完整代码 index.html文件 style.css文件 总结 前言 随着前端技术的不断发展与进步&#xff0c;界面交互的样式要求和美感也越来越高&#xff0c;很多网页的交互都加上了css动画,这里作者给大家分享一…

【参加CUDA线上训练营】共享内存实例1:矩阵转置实现及其优化①

【参加CUDA线上训练营】共享内存实例1&#xff1a;矩阵转置实现及其优化①1.完整代码2.原理介绍2.1 将各block 线程对应元素放入共享内存tile2.2 实现转置2.3 在此基础上修改参考文献本文参考Nvidia官方blog[An Efficient Matrix Transpose in CUDA C/C及其对应的github代码tra…

表情包可视化编辑、生成配置信息数据工具

合成GIF图片 - 表情包 后续&#xff0c;用于快速、便捷生成 img_config.js 中 要生成的GIF每一帧数据&#xff08;写入头像图片信息参数&#xff09;&#xff1b; 1、先上传 写入GIF中头像 标准图&#xff0c;同时获取图片信息&#xff0c;更新 写入GIF中头像 初始值&#xff0…

5-HT2A靶向药物|适应症|市场销售-上市药品前景分析

据世界卫生组织称&#xff0c;抑郁症是一种多因素疾病&#xff0c;影响全球约3.5 亿人。中枢神经系统最广泛的单胺 - 血清素 (5-HT) 被认为在这种情况的病理机制中起着至关重要的作用&#xff0c;并且神经递质的重要性被“血清素假说”提升&#xff0c;将抑郁症的存在联系起来 …

配置Qt Creator

前言 为了使Qt Creator更像您最喜欢的代码编辑器或IDE&#xff0c;您可以更改键盘快捷键、配色方案、通用高亮显示、代码片段和版本控制系统的设置。 检查生成和运行设置 Qt Creator是一个集成开发环境(IDE)&#xff0c;可以用来开发Qt应用程序。虽然您可以使用Qt Installer…

聊天不发表情包会不习惯吗,Python带你轻松采集上万个表情包

前言 (&#xff61;&#xff65;∀&#xff65;)&#xff89;&#xff9e;嗨 大家好&#xff0c;这里是小圆 聊天没表情包你会有点不习惯的感jio吗 就比如新注册了个qq或者微信再或者企业微信 emmm我就是这样拿到新账号后&#xff0c;跟别人聊聊天想发送表情包 &#xff0c;…

C++-类和对象(下)

C-类和对象&#xff08;下&#xff09;一&#xff0c;const成员函数二&#xff0c;再谈构造函数1&#xff0c;初始化列表2&#xff0c;explicit关键字三&#xff0c;static成员四&#xff0c;友元&#xff08;friend&#xff09;1&#xff0c;全局函数做友元2&#xff0c;类做友…

冷冻电镜 - ChimeraX Density Map 密度图 操作

欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://blog.csdn.net/caroline_wendy/article/details/129055160 由冷冻电镜所生成的Volume,需要观察其内部结构,使用ChimeraX进行操作。 加载Volumes,例如my_volume.mrc 效果如下: 高斯滤波 在命令行(Co…

python 数据分析可视化实战 超全 附完整代码数据

代码数据&#xff1a;https://download.csdn.net/download/qq_38735017/873799141.1 数据预处理1.1.1 异常值检测①将支付时间转为标准时间的过程中发生错误&#xff0c;经排查错误数据为‘2017/2/29’,后将其修改为‘2017/2/27’。②经检测发现部分订单应付金额与实付金额都为…

解决jupyter以及windows系统中pycharm编译器画图的中文乱码问题大全

一、jupyter环境下中文乱码问题解决 我们在jupyter的notebook中使用matplotlib画图的时候&#xff0c;经常性的会遇见一些中文乱码显示□的情况,如下所示: 在此&#xff0c;网上给出的方法大多是以下的解决方法&#xff1a; import matplotlib.pyplot as pltplt.rcParams[fo…

界面组件Telerik UI for WinForms R1 2023——全新的Windows 11主题

Telerik UI for WinForms拥有适用Windows Forms的110多个令人惊叹的UI控件。所有的UI for WinForms控件都具有完整的主题支持&#xff0c;可以轻松地帮助开发人员在桌面和平板电脑应用程序提供一致美观的下一代用户体验。Telerik UI for WinForms组件发布了2023年第一个重大版本…

QCon演讲实录(下):多云管理关键能力实现与解析-AppManager

在上篇中&#xff0c;我们已经基本了解了多云管理。现在&#xff0c;我们将深入探讨多云管理关键能力实现&#xff1a;AppManager。 什么是AppManager&#xff1f; 上面我们讲了理论、我们自己使用的交付流程和整体架构&#xff0c;下面我们进入关键能力实现与解析的环节&…

Allegro如何通过视图显示区分动态和静态铜皮操作指导

Allegro如何通过视图显示区分动态和静态铜皮操作指导 用Allegro做PCB设计的时候,通常动态和静态铜皮是无法通过视图显示区分的,只能通过show element查看得知,如下图 左边铜皮是动态铜皮,右边是静态铜皮 但Allegro可以通过一些设置让动静态铜皮以不同效果显示出来 具体操…

Elasticsearch:使用 intervals query - 根据匹配项的顺序和接近度返回文档

Intervals query 根据匹配项的顺序和接近度返回文档。Intervals 查询使用匹配规则&#xff0c;由一小组定义构成。 然后将这些规则应用于指定字段中的术语。 这些定义产生跨越文本正文中的术语的最小间隔序列。 这些间隔可以通过父源进一步组合和过滤。 上述描述有点费解。我…

【计算机网络】HTTPS协议原理

文章目录一、认识HTTPS协议二、为什么要发明HTTPS三、HTTP与HTTPS的区别四、常见的加密方式1. 对称加密2. 非对称加密3. 数据摘要4. 数字签名五、HTTPS的原理探究方案1&#xff1a;只使用对称加密方案2&#xff1a;只使用非对称加密方案3&#xff1a;双方都使用非对称加密方案4…

2.15学习总结

上次被学长的问题给问住了&#xff0c;突然发现自己动规有点糊涂&#xff0c;然后就去屁颠屁颠的复习&#xff0c;找几个之前做过的题&#xff0c;突然发现&#xff0c;竟然还是写了好久才写出来&#xff0c;怎么说呢&#xff0c;信心被强烈打击到&#xff0c;然后自己找了一个…

MyBatis 之二(增、删、改操作)

文章目录1. 修改操作1.1 在 mapper&#xff08;interface&#xff09;里面添加修改方法的声明1.2 在 XMl 中添加 <update> 标签和修改的 sql 代码1.3 在 UserMapper 中右键 Generate 点击 Test 生成 update 测试类2. 删除操作2.1 在 mapper &#xff08;interface&#x…