在互联网下半场,数据量的爆发式增长与业务对高可用的极致追求,让数据库管理员(DBA)的角色从单纯的“背锅侠”转型为系统的“架构师”。在刚刚结束的第四期大厂 DBA 实战班中,我们从内核原理到架构演进,深度剖析了 Redis、MySQL 等核心组件在大厂生产环境中的最佳实践。
本文将萃取本期实战班的精华内容,重点围绕“高并发锁竞争”与“海量数据分库分表”两大核心痛点,分享硬核干货与实战代码。
一、 Redis 进阶:超越 setnx 的分布式锁优化
在实战班的“Redis 深度历险”模块,我们指出许多初级开发者甚至资深工程师在实现分布式锁时,仍停留在简单的 SETNX 层面。这种做法存在死锁风险、原子性缺陷以及无法自动续期的问题。
大厂标准方案: 引入 Redisson 客户端,利用其封装的“看门狗”机制实现锁的自动续期,并利用 Lua 脚本保证加锁/解锁的原子性。
以下是基于 Redisson 的分布式锁实战代码:
java
复制
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
@RestController
public class InventoryController {
@Autowired
private Redisson redisson;
/**
* 扣减库存场景:模拟大并发下的安全扣减
* 核心干货:
* 1. 比起手写 Lua 脚本,Redisson 实现了可重入锁
* 2. watchDog 机制:锁默认 30 秒过期,业务未跑完会自动续期,防止死锁
* 3. WaitTime:获取锁的最大等待时间(防止一直阻塞)
*/
@GetMapping("/deduct/stock")
public String deductStock() {
String lockKey = "product:101:lock";
// 获取锁对象
RLock lock = redisson.getLock(lockKey);
try {
// 尝试加锁:waitTime=0(不等待), leaseTime=10(10秒后自动释放), TimeUnit=SECONDS
boolean isLocked = lock.tryLock(0, 10, TimeUnit.SECONDS);
if (!isLocked) {
return "系统繁忙,请稍后重试"; // 快速失败机制,保护数据库
}
// --- 业务逻辑开始 ---
System.out.println("获取锁成功,执行扣减操作...");
// TODO: 执行 MySQL update 操作...
// --- 业务逻辑结束 ---
return "扣减成功";
} catch (InterruptedException e) {
e.printStackTrace();
return "系统异常";
} finally {
// 关键点:锁必须放在 finally 释放,且需判断是否持有锁(防止误删)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
DBA 视角解读: 这段代码的核心价值在于引入了 leaseTime 和 tryLock,解决了传统 SET key value PX 30000 NX 在业务宕机时无法释放锁导致的死锁问题,同时也避免了主从切换导致锁丢失的风险。
二、 MySQL 进阶:分库分表后的分页查询难题
当单表数据突破千万级,简单的读写分离已无法支撑写入压力,必须进行水平分表。但在实战班中,最让学员头疼的莫过于“分页查询”。
痛点分析: SELECT * FROM table ORDER BY id LIMIT 100000, 10。在未分表时,MySQL 需要扫描 100010 行记录。在分表后(假设分为 4 张表),我们需要对 4 张表分别查询,再在内存中合并排序,性能呈指数级下降。
大厂标准方案: 禁止使用大的 OFFSET,改用“游标分页”或“ID 范围查询”。
以下是基于 MyBatis 的 ID 范围查询实战
java
复制
// Service 层逻辑
public List<User> selectPageByCursor(Long lastMaxId, Integer pageSize) {
List<User> resultList = new ArrayList<>();
// 假设分表数量为 4,逻辑分表规则为 userId % 4
// 这里演示简化版,实际生产中通常通过分片中间件(如 ShardingSphere)处理
for (int i = 0; i < 4; i++) {
// 并行查询所有分片
List<User> subList = userMapper.selectPageByLastId(i, lastMaxId, pageSize);
resultList.addAll(subList);
}
// 内存归并排序(虽然每张表是有序的,但表之间需要整体排序)
// 在大厂实践中,通常由数据库中间件完成 Stream 归并,业务层只需专注逻辑
resultList.sort(Comparator.comparing(User::getId));
// 取前 pageSize 条
return resultList.stream().limit(pageSize).collect(Collectors.toList());
}
DBA 视角解读: 这里的关键在于利用 B+ 树的特性。WHERE id > last_id 直接利用了聚簇索引,避免了 OFFSET 带来的回表扫描与文件排序。这是在大数据量分页场景下,唯一能保持 QPS 稳定的方案。
三、 架构演进:从主从复制到 MGR
在课程的最后阶段,我们还探讨了 MySQL 组复制(MGR)在高可用架构中的应用。相比于传统的异步复制,MGR 提供了强一致性保证,解决了金融级业务中“主库宕机数据丢失”的终极难题。
核心配置心得:
- 设置
group_replication_consistency='EVENTUAL'还是'BEFORE_ON_PRIMARY_FAILOVER'需要严格评估业务对一致性延迟的容忍度。 - 在单主模式下,利用路由组件(如 ProxySQL 或 MySQL Router)自动屏蔽故障节点,实现应用层的无感切换。
结语
第四期大厂 DBA 实战班的干货不仅在于代码的编写,更在于对数据一致性、系统可用性、扩展性这三者之间权衡的艺术。掌握了 Redis 锁的正确用法与分页查询的底层优化,仅仅是迈出了进阶的第一步。真正的专家,是在每一次查询慢日志的背后,看到的是磁盘 IO、网络抖动与内存管理的交响曲。