一、索引
索引像书的目录:不用翻全表,直接定位行,数据库的索引设计默认是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.索引失效
LIKE '%张%':B+树匹配时不知道关键词的前缀,导致索引无效。- 联合匹配不满足最左原理
二、事务
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>