1. 锁机制的本质与分类
数据库锁和缓存锁作为两种典型的并发控制手段,在技术面试中经常被拿来对比。我曾在电商秒杀系统架构设计中同时使用过MySQL行锁和Redis分布式锁,深刻体会到两者的差异点。锁本质上是通过对共享资源的访问限制,解决并发场景下的数据一致性问题。
1.1 悲观锁与乐观锁实现原理
MySQL同时支持悲观锁和乐观锁两种模式。悲观锁的代表是SELECT ... FOR UPDATE语句,其工作原理是在事务开始时就直接锁定目标数据行。我在处理账户余额变更时常用这种方式:
BEGIN; SELECT balance FROM accounts WHERE user_id = 1001 FOR UPDATE; -- 执行业务逻辑 UPDATE accounts SET balance = new_value WHERE user_id = 1001; COMMIT;而乐观锁则是通过版本号机制实现,典型实现如下:
UPDATE products SET stock = stock - 1, version = version + 1 WHERE product_id = 2001 AND version = old_version;Redis作为内存数据库,原生只支持悲观锁。其SETNX命令是实现锁的原子性操作:
SETNX lock:order_123 true # 返回1表示获取锁成功1.2 锁的粒度对比分析
MySQL的锁粒度可以细分为:
- 表级锁:MyISAM引擎默认
- 行级锁:InnoDB支持
- 间隙锁:防止幻读
Redis的锁粒度则取决于key设计:
- 全局锁:单个key控制整个系统
- 业务锁:
lock:业务名:ID形式 - 细粒度锁:对复合操作分段加锁
在订单超时处理系统中,我采用分层锁设计:先用Redis锁住订单ID防止重复处理,再用MySQL行锁保证数据一致性。这种组合方案将并发能力提升了3倍。
2. Redis分布式锁深度解析
2.1 正确实现分布式锁的五个要点
原子性获取:必须使用SETNX+过期时间组合命令
SET lock:res001 uuid EX 30 NX唯一标识:每个客户端使用唯一UUID,防止误删
String clientId = UUID.randomUUID().toString();自动释放:必须设置过期时间,我建议根据业务耗时动态调整
续期机制:通过看门狗线程定期延长锁时间
while keep_alive: redis.expire(lock_key, 30) time.sleep(10)释放验证:删除前校验持有者身份
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) end
2.2 典型问题场景与解决方案
缓存雪崩:大量锁同时过期导致请求暴增
- 解决方案:给过期时间添加随机值
EXPIRE lock:order123 30 + rand(0,5)
锁等待风暴:高并发下大量线程轮询抢锁
- 优化方案:采用Redisson的订阅发布机制
RLock lock = redisson.getLock("lock"); lock.lock(); try { // 业务代码 } finally { lock.unlock(); }
在日活百万的社交APP中,我们通过Redisson的联锁(MultiLock)实现了跨节点的事务控制,错误率从5%降至0.1%以下。
3. MySQL锁机制实战剖析
3.1 InnoDB锁类型工作原理
记录锁(Record Lock):
- 锁定索引记录
- 即使表无索引也会创建隐藏聚簇索引
间隙锁(Gap Lock):
- 锁定索引记录间的区间
- 防止其他事务插入导致幻读
临键锁(Next-Key Lock):
- 记录锁+间隙锁组合
- 默认的InnoDB锁模式
在一次库存超卖事故排查中,我发现事务隔离级别对锁行为影响巨大:
-- 会话A SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; BEGIN; SELECT * FROM products WHERE id > 100 FOR UPDATE; -- 会话B(会被阻塞) INSERT INTO products(id) VALUES(150);3.2 死锁检测与处理方案
MySQL通过等待图(wait-for graph)检测死锁,但DBA还需要掌握手动分析方法:
查看死锁日志
SHOW ENGINE INNODB STATUS;关键指标监控
SELECT * FROM performance_schema.events_waits_current;应急处理方案
- 设置超时:
innodb_lock_wait_timeout=50 - 索引优化:为高频查询字段添加索引
- 事务拆分:大事务拆分为小事务
- 设置超时:
在金融系统中,我们通过以下配置将死锁率降低了90%:
innodb_deadlock_detect = ON innodb_print_all_deadlocks = ON transaction_isolation = READ-COMMITTED4. 混合锁架构设计实践
4.1 电商库存扣减方案对比
纯MySQL方案:
BEGIN; SELECT stock FROM items WHERE id=100 FOR UPDATE; UPDATE items SET stock=stock-1 WHERE id=100 AND stock>0; COMMIT;- 优点:强一致性
- 缺点:QPS上限约2000
Redis+MySQL方案:
def deduct_stock(): redis_lock = acquire_redis_lock("item_100") if not redis_lock: return False try: stock = redis.decr("stock:100") if stock < 0: redis.incr("stock:100") return False async_update_mysql() # 异步更新数据库 return True finally: release_redis_lock("item_100")- 优点:QPS可达2万+
- 缺点:存在短暂数据不一致窗口
4.2 分布式事务中的锁应用
在微服务架构下,我们采用Saga模式配合锁机制:
- 订单服务:Redis锁防止重复创建
- 库存服务:MySQL行锁保证准确扣减
- 支付服务:Redis锁+本地事务表
关键补偿机制设计:
@Transactional public void cancelOrder(Long orderId) { // 1. 获取分布式锁 String lockKey = "order_compensate:" + orderId; if (!redisLock.tryLock(lockKey, 30, TimeUnit.SECONDS)) { throw new RetryableException(); } try { // 2. 查询订单状态 Order order = orderDao.selectForUpdate(orderId); // 3. 执行补偿逻辑 if (order.getStatus() == Status.PAID) { paymentService.refund(order); inventoryService.release(order); } // 4. 更新订单状态 orderDao.updateStatus(orderId, Status.CANCELLED); } finally { redisLock.unlock(lockKey); } }5. 性能优化关键指标
5.1 Redis锁监控要点
锁等待时间
redis-cli --latency -p 6379锁持有时间分布
SELECT FLOOR(duration/100)*100 AS duration_range, COUNT(*) AS count FROM lock_log GROUP BY 1;锁冲突热力图
from redis import Redis r = Redis() hot_keys = r.memory_usage("lock:*", samples=1000)
5.2 MySQL锁优化checklist
索引检查
EXPLAIN SELECT * FROM orders WHERE user_id=100 FOR UPDATE;锁升级监控
SELECT * FROM sys.innodb_lock_waits;事务持续时间
SELECT AVG(TIMESTAMPDIFF(SECOND,trx_started,NOW())) FROM information_schema.INNODB_TRX;
在物流系统中,我们通过以下调整将锁等待时间从800ms降至50ms:
- 为所有高频查询添加组合索引
- 将大事务拆分为多个<100ms的小事务
- 把REPEATABLE READ改为READ COMMITTED