04-数据库-MySQL

一、索引

索引像书的目录:不用翻全表,直接定位行,数据库的索引设计默认是B+树。

1.1 B+树

B+树核心特点:所有的数据都在叶子层,数据通过链表有序的连接。

假设寻找数据7,从根节点开始,9>7,则走左侧查询,5<7走右侧查询,命中找到了链表节点7。

1.2 索引类型

数据库常规索引类型有4类

类型 写法 是否允许重复 一张表几个
主键 PRIMARY KEY 否,也不能 NULL 只能 1 个
唯一索引 UNIQUE KEY 否(NULL 在 InnoDB 可有多个) 可以多个
普通索引 INDEX / KEY 可以 可以多个
联合索引 (status, id) 看你是否加 UNIQUE 可以多个

唯一索引

sql 复制代码
ALTER TABLE user ADD UNIQUE KEY uk_username (username);

普通索引

sql 复制代码
ALTER TABLE user ADD INDEX idx_status (status);

联合索引

联合索引支持给多个字段添加索引,它必须符合最左原则,例如(a,b)索引时,即支持a匹配,也支持a,b组合匹配,但是不支持b单独匹配。

联合索引 复制代码
CREATE INDEX idx_status_id ON user (status, id);

使用示例

其中1和2会命中索引查询,但是3不会命中索引属于无效索引。

sql 复制代码
select * from user where status=1;
select * from user where status=1 and id=1;
select * from user where id=1;

1.4 索引排查

索引属于数据库的优化手段,但并不是无脑添加索引一定会提高sql的执行效率。因此需要掌握索引效率与日志排查

1.EXPLAIN 判断效果

explain是sql中预演工具,不是执行真正的SQL查询,它可以分析出这条SQL执行会不会走索引,检索多少行。重点从如下几个关键指标分析。

含义 枚举值
type 访问方式,表示只访问几行还是索引访问还是全表访问 const>ref>range>index>all
key 实际用了哪个索引。NULL 往往没用上索引 PRIMARY、自定义索引、null
Extra 额外动作,例如额外排序,临时表 Using index>Using filesort>Using temporary

type类型是all一定要警惕,它表示走了全表扫描。key类型是null要警惕,表示没有走索引。extra类型是using filesort和Using temporary要警惕表示做了额外的排序和临时表对比动作。

sql分析时优先看到指标顺序依次是key>type>extra

2.案例演示

查询用户表同时满足status和id符合条件的用户,如下是添加索引后前后对比,可以看到type和extra的值明显优化。

sql 复制代码
EXPLAIN SELECT id, username FROM user WHERE status = 1 ORDER BY id LIMIT 10;
属性 未加索引 增加联合索引(id,status)
key PRIMARY idx_status_id
type index ref
extra use where null

3.索引失效

  1. LIKE '%张%':B+树匹配时不知道关键词的前缀,导致索引无效。
  2. 联合匹配不满足最左原理

二、事务

2.1 概念

一组 SQL 要么全成功,要么全当没发生过。中间崩了,不能出现改了一半的数据。

2.2 使用场景

并不是所有场景都需要添加事务,单次的crud肯定没必要开事务,同生共死的场景肯定是需要事务的,例如a读取支付宝余额,b执行扣款操作,这种必须加事务。

java中使用

直接在方法头部添加Transactional注解即可

java 复制代码
private void updateThenBoom(Long id) {
    User user = userMapper.findById(id);
    if (user == null) {
        throw new BusinessException(404, "用户不存在: " + id);
    }
    userMapper.updateEmail(id, FAIL_EMAIL);
    throw new BusinessException(500,
            "模拟事务失败:已执行 UPDATE email=" + FAIL_EMAIL + ",请再查 GET /api/users/" + id);
}

@Transactional(rollbackFor = Exception.class)
public void failAndRollback(Long id) {
    updateThenBoom(id);
}

2.3 隔离级别

事务的隔离级别有4类,常用的有2个。理解隔离级别前先得搞清楚数据库事务的执行原理,借事务可以模拟隔离级别。

如下是一条sql的执行,日常sql的默认就是执行了事务的start和commit,commit就是提交的概念。

sql 复制代码
START TRANSACTION;
SELECT email FROM user WHERE id = 1;
COMMIT

2.3.1 RC 读已提交

核心解释:我的事务里每次读,都只读别人已经提交的最新数据,别人没有提交的数据我永远读取不到最新的。

现在有两个同学a,b操作数据库,id=1的user的email初始是dzp

步骤1

a同学开启事务的start,但是没有提交,查询的email是dzp,

sql 复制代码
START TRANSACTION;
SELECT email FROM user WHERE id = 1;

步骤2

b同学开启事务,提交了,修改了id=1的email

sql 复制代码
UPDATE user SET email = 'b@test.com' WHERE id = 1;

步骤3

a同学再查询结果,返回的是email是b@test.com

sql 复制代码
SELECT email FROM user WHERE id = 1;
COMMIT;

2.3.2 RR 可重复度(默认)

我的事务里读取操作时,无论对方是否提交,我的多次读都只会读第一次读取的快照数据。

现在有两个同学a,b操作数据库,id=1的user的email初始是dzp

步骤1

a同学开启事务的start,但是没有提交,查询的email是dzp,

sql 复制代码
START TRANSACTION;
SELECT email FROM user WHERE id = 1;

步骤2

b同学开启事务,提交了,修改了id=1的email

sql 复制代码
UPDATE user SET email = 'b@test.com' WHERE id = 1;

步骤3

a同学再查询结果,返回的是email是dzp

sql 复制代码
SELECT email FROM user WHERE id = 1;
COMMIT;

2.4 事务失效

只有经过spring代理调用的事务才会生效。通过类的this或其他类调用事务是无效的。

案例1

下面案例updateWrong方法调用了带事务的doUpdate方法,这种就属于无效。这里可以直接在controller层直接调用doUpdate就是成功的案例。

Java 复制代码
@Service
public class TransactionLabService {

    private final UserMapper userMapper;

    public TransactionLabService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public void updateWrong(Long id, String email) {
        doUpdate(id, email);  // this.doUpdate → 无事务
    }

    @Transactional
    public void doUpdate(Long id, String email) {
        User u = userMapper.findById(id);
        u.setEmail(email);
        userMapper.updateEmail(u);
        int x = 1 / 0;  // 故意异常
    }
}

2.5 更新DB+删缓存

db查询时新建缓存,db更新则删除缓存(保证db数据与缓存一致性)。但在事务存在时,更新DB+删除缓存是十分容易出错。

无事务情况

没有事务时,直接在方法头部添加@CacheEvict,spring就自动在sql提交后才删除缓存

Java 复制代码
@CacheEvict(value = "user", key = "#id")
public void delete(Long id) {
    if (userMapper.deleteById(id) == 0) {
        throw new RuntimeException("用户不存在: " + id);
    }
}

有事务

核心保证事务在db更新提交后,才能执行缓存的删除。因此使用TransactionSynchronizationManager.registerSynchronization来监听事务结束后才执行缓存删除。

java 复制代码
@Transactional(rollbackFor = Exception.class)
public void update(User user) {
    User existing = userMapper.findById(user.getId());
    if (existing == null) {
        throw new BusinessException(404, "用户不存在: " + user.getId());
    }
    if (userMapper.updateWithVersion(user) == 0) {
        throw new BusinessException(400, "用户已被修改: " + user.getId());
    }

    Long id = user.getId();
    TransactionSynchronizationManager.registerSynchronization(
        new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                // 和 @Cacheable 的 cache name 一致:user::1
                cacheManager.getCache("user").evict(id);
            }
        });
}

三、锁

数据库的锁保证同一时刻,同一份资源只允许按规则被改。常见的锁有两类:乐观锁、悲观锁。

悲观锁 乐观锁
含义 觉得肯定撞车,先占住再修改 先尝试修改,大不了失败再试
适用场景 突多、必须排队(秒杀库存) 冲突少(改用户资料、改配置)
代价 锁等、事务要短,否则容易超时/死锁 几乎不加锁,但要处理「请刷新后重试」

3.1 悲观锁

模拟两人共同操作同一份数据的悲观锁场景(数据库更新db都属于排它锁,其它人不允许同时操作),所以只需要给查询语句添加悲观锁

1.a开启事务

a给查询id=1这条数据加了悲观锁

sql 复制代码
START TRANSACTION;
SELECT * FROM user WHERE id = 1 FOR UPDATE;

2.b开启事务

b在使用悲观锁查询时,会被阻塞,只有a执行commit后才会释放锁,b才能继续执行查询。

sql 复制代码
START TRANSACTION;
SELECT * FROM user WHERE id = 1 FOR UPDATE;

Java案例

需要根据用户ID和新的资料去更新,所以首先需要判断用户是否存在,才能继续更新资料,因此需要添加事务,其次模拟悲观锁,如果其它人也在操作这个用户修改时,会被阻塞。

sql 复制代码
@Transactional(rollbackFor = Exception.class)
public void updateWithPessimisticLock(User user) {
    User existingUser = userMapper.findByIdForUpdate(user.getId());
    if (existingUser == null) {
        throw new BusinessException(404, "用户不存在: " + user.getId());
    }
    userMapper.updateEmail(user.getId(), user.getEmail());
}

 <select id="findByIdForUpdate" resultType="User">
    SELECT id, username, password, email, phone, version
    FROM user
    WHERE id = #{id}
    FOR UPDATE
</select>

3.2 乐观锁

乐观锁的控制通过版本号,每条数据都有版本号字段,用户在更新数据时需要带上版本号去更新,如果发现没命中,说明有人已经改了这个版本,那这次更新就失败了,需要重新尝试了。

Java 复制代码
@Transactional
public void updateUser(Long id, String email, String phone) {
    User user = userMapper.findById(id);
    if (user == null) {
        throw new BusinessException(404, "用户不存在");
    }
    user.setEmail(email);
    user.setPhone(phone);
    int rows = userMapper.updateWithVersion(user);
    if (rows == 0) {
        // 别人先改过了,version 对不上
        throw new BusinessException(409, "数据已被修改,请刷新后重试");
    }
}

<update id="updateWithVersion">
    UPDATE user
    SET email = #{email},
        phone = #{phone},
        version = version + 1
    WHERE id = #{id}
      AND version = #{version}
</update>
相关推荐
她的男孩1 小时前
接口加密做成框架级能力有多难?我扒了 3600 行源码:从 RSA 握手到落库密文迁移
人工智能·后端·架构
这个DBA有点耶1 小时前
COUNT慢不是因为用了*,是这5个原因——1000万行数据实测+执行计划深度解析
数据库·mysql·算法
掘金挖土1 小时前
前端手摸手跑路之 AI 应用开发(二)
前端·后端
kyrie_sakura2 小时前
MySQL数据库学习笔记2--系统函数(分组,单行,窗口函数)
数据库·学习·mysql
遨翔在知识的海洋里2 小时前
nest(5)-文件上传和静态资源
后端
遨翔在知识的海洋里2 小时前
nest(3)-jwt和RBAC
后端
遨翔在知识的海洋里2 小时前
nest(5)-Middleware
后端
程序员梅雨3 小时前
Linux & Shell 实用干货
linux·运维·服务器·后端·面试·php
大牧师3 小时前
TypeORM 入门教程
后端·sql·mysql·orm·nest·typeorm