Redis

关注公众号 jb51net

关闭
首页 > 数据库 > Redis > Redis锁与DB锁

Redis锁与DB锁的使用与区别小结

作者:AlbenXie

本文主要介绍了Redis锁与DB锁的使用与区别小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、Redis 锁与 DB 锁的对比

Redis 锁(分布式锁)和 DB 锁(数据库锁)是分布式场景下控制并发的核心手段,二者在实现方式、性能、可靠性、适用场景上差异显著:

维度

Redis 锁(如 SETNX/Redlock)

DB 锁(如行锁 / 表锁 / 唯一索引)

实现方式

1. 基础版:SETNX key value EX 过期时间(单节点)

2. 高级版:Redlock(多节点 Redis 集群)

1. 行锁:SELECT FOR UPDATE(悲观锁)、版本号(乐观锁)

2. 表锁:LOCK TABLES

3. 唯一索引:通过唯一约束实现分布式锁

性能

极高(内存操作,QPS 可达 10 万 +),加锁 / 解锁耗时微秒级

较低(磁盘 IO + 事务开销),加锁 / 解锁耗时毫秒级,高并发下易成为瓶颈

可靠性

1. 单节点:Redis 宕机则锁失效(需设置过期时间兜底)

2. Redlock:多节点降低宕机风险,但仍存在时钟漂移问题

高(数据库事务 ACID 保障,宕机后恢复数据不丢失),锁由数据库事务机制保障

锁粒度

粗粒度(按 key 锁,可自定义粒度,如用户 ID、订单号)

细粒度(行锁可锁定单条记录,表锁粒度最粗)

过期机制

支持自动过期(EX 参数),可防止死锁

1. 悲观锁:依赖事务提交 / 回滚释放,若事务卡住则死锁

2. 乐观锁:无过期,靠版本号控制

分布式支持

天然支持分布式(Redis 集群),跨服务 / 跨机器

支持分布式(数据库主从 / 集群),但跨库锁需额外处理(如 XA 事务)

死锁风险

低(过期时间自动释放),但可能出现锁过期导致并发问题(如业务处理时间超过过期时间)

高(悲观锁),需依赖数据库死锁检测机制自动解除,或人工干预

适用场景

1. 高并发场景(如秒杀、支付)

2. 非核心数据的并发控制

3. 短期锁(业务处理时间短)

1. 低并发场景

2. 核心数据的并发控制(如资金变动)

3. 长期锁(业务处理时间长)

4. 需要事务保障的场景

实现复杂度

中等(需处理锁过期、重入、释放别人的锁等问题,可使用 Redisson 框架)

低(悲观锁直接用 SELECT FOR UPDATE,唯一索引只需建表)

二、举例说明

我们通过电商秒杀场景金融资金扣减场景两个典型案例,结合代码实现和问题分析,深入对比 Redis 锁与 DB 锁的差异、适用场景及坑点。

先明确核心概念

案例 1:电商秒杀场景(高并发、短事务)

业务背景

某电商平台秒杀 iPhone,库存只有 100 台,每秒有 10 万 + 请求,需要控制并发下单,防止超卖。

方案 1:使用 Redis 锁实现

1. 核心实现(基于 Redisson,解决原生 Redis 锁的坑)

Redisson 是 Redis 的 Java 客户端,封装了分布式锁的实现,自动处理锁过期、重入、释放别人的锁、集群高可用等问题。

@Service
public class SeckillService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
 
    // 秒杀核心方法
    public String seckill(String productId, String userId) {
        // 1. 定义Redis锁key(粒度:商品ID,确保同一商品的秒杀串行)
        String lockKey = "seckill_lock:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
 
        try {
            // 2. 获取锁(等待时间10秒,锁自动过期30秒,防止死锁)
            boolean lockSuccess = lock.tryLock(10, 30, TimeUnit.SECONDS);
            if (!lockSuccess) {
                return "秒杀太火爆了,请稍后重试!";
            }
 
            // 3. 业务逻辑:先查Redis库存(缓存),再扣减,最后同步到DB
            Integer stock = redisTemplate.opsForValue().get("seckill_stock:" + productId);
            if (stock == null || stock <= 0) {
                return "秒杀已结束!";
            }
            // 扣减Redis库存
            redisTemplate.opsForValue().decrement("seckill_stock:" + productId);
            // 生成订单(异步写入DB,提升性能)
            seckillMapper.createOrder(productId, userId);
            return "秒杀成功!";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "秒杀失败,请重试!";
        } finally {
            // 4. 释放锁(只有持有锁的线程才能释放)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

2. Redis 锁在秒杀场景的优势

3. Redis 锁的坑点及解决

方案 2:使用 DB 锁(SELECT FOR UPDATE)实现

1. 核心实现

@Service
public class SeckillService {
    @Autowired
    private SeckillMapper seckillMapper;
 
    // 秒杀核心方法(事务内执行)
    @Transactional(rollbackFor = Exception.class)
    public String seckill(String productId, String userId) {
        // 1. 获取DB行锁(根据商品ID锁定库存记录,悲观锁)
        SeckillStock stock = seckillMapper.selectStockByProductIdForUpdate(productId);
        if (stock == null || stock.getStock() <= 0) {
            return "秒杀已结束!";
        }
 
        // 2. 扣减库存
        seckillMapper.decrementStock(productId);
        // 3. 生成订单
        seckillMapper.createOrder(productId, userId);
        return "秒杀成功!";
    }
}
 
// Mapper接口
public interface SeckillMapper {
    @Select("SELECT * FROM seckill_stock WHERE product_id = #{productId} FOR UPDATE")
    SeckillStock selectStockByProductIdForUpdate(String productId);
 
    @Update("UPDATE seckill_stock SET stock = stock - 1 WHERE product_id = #{productId}")
    int decrementStock(String productId);
 
    @Insert("INSERT INTO seckill_order (product_id, user_id) VALUES (#{productId}, #{userId})")
    void createOrder(String productId, String userId);
}

2. DB 锁在秒杀场景的劣势

3. 结论:秒杀场景优先用 Redis 锁

Redis 锁的性能和并发支撑能力远优于 DB 锁,适合高并发、短事务的场景,即使存在少量锁过期风险,也可通过业务兜底(如库存最终一致性校验)解决。

案例 2:金融资金扣减场景(低并发、核心数据、长事务)

业务背景

某银行 APP 的用户转账功能,用户 A 向用户 B 转账 1 万元,需要扣减 A 的余额,增加 B 的余额,要求资金绝对不能出错(不能多扣、少扣、重复扣)。

方案 1:使用 DB 锁(SELECT FOR UPDATE)实现

1. 核心实现

@Service
public class TransferService {
    @Autowired
    private AccountMapper accountMapper;
 
    // 转账核心方法(事务内执行,保证原子性)
    @Transactional(rollbackFor = Exception.class)
    public String transfer(String fromUserId, String toUserId, BigDecimal amount) {
        // 1. 校验金额
        if (amount.compareTo(BigDecimal.ZERO) <= 0) {
            return "转账金额必须大于0!";
        }
 
        // 2. 获取用户A的行锁(扣减余额前加锁,防止并发扣减)
        Account fromAccount = accountMapper.selectByUserIdForUpdate(fromUserId);
        if (fromAccount == null) {
            return "转出账户不存在!";
        }
        // 校验余额
        if (fromAccount.getBalance().compareTo(amount) < 0) {
            return "余额不足!";
        }
 
        // 3. 获取用户B的行锁(防止并发更新)
        Account toAccount = accountMapper.selectByUserIdForUpdate(toUserId);
        if (toAccount == null) {
            return "转入账户不存在!";
        }
 
        // 4. 扣减用户A的余额
        accountMapper.decrementBalance(fromUserId, amount);
        // 5. 增加用户B的余额
        accountMapper.incrementBalance(toUserId, amount);
 
        return "转账成功!";
    }
}
 
// Mapper接口
public interface AccountMapper {
    @Select("SELECT * FROM account WHERE user_id = #{userId} FOR UPDATE")
    Account selectByUserIdForUpdate(String userId);
 
    @Update("UPDATE account SET balance = balance - #{amount} WHERE user_id = #{userId}")
    int decrementBalance(String userId, BigDecimal amount);
 
    @Update("UPDATE account SET balance = balance + #{amount} WHERE user_id = #{userId}")
    int incrementBalance(String userId, BigDecimal amount);
}

2. DB 锁在资金扣减场景的优势

3. DB 锁的优化点

方案 2:使用 Redis 锁实现

1. 核心实现

@Service
public class TransferService {
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private AccountMapper accountMapper;
 
    public String transfer(String fromUserId, String toUserId, BigDecimal amount) {
        // 1. 定义Redis锁key(粒度:用户ID,按字典序加锁)
        String lockKey1 = "transfer_lock:" + (fromUserId.compareTo(toUserId) < 0 ? fromUserId : toUserId);
        String lockKey2 = "transfer_lock:" + (fromUserId.compareTo(toUserId) > 0 ? fromUserId : toUserId);
        RLock lock1 = redissonClient.getLock(lockKey1);
        RLock lock2 = redissonClient.getLock(lockKey2);
 
        try {
            // 2. 批量获取锁(等待时间10秒,锁过期30秒)
            boolean lockSuccess = RedissonMultiLock(lock1, lock2).tryLock(10, 30, TimeUnit.SECONDS);
            if (!lockSuccess) {
                return "转账请求处理中,请稍后重试!";
            }
 
            // 3. 业务逻辑(扣减+增加余额,无事务保障)
            Account fromAccount = accountMapper.selectByUserId(fromUserId);
            if (fromAccount == null || fromAccount.getBalance().compareTo(amount) < 0) {
                return "余额不足或账户不存在!";
            }
            Account toAccount = accountMapper.selectByUserId(toUserId);
            if (toAccount == null) {
                return "转入账户不存在!";
            }
 
            // 4. 扣减余额(无事务,可能出现扣减成功但增加失败)
            accountMapper.decrementBalance(fromUserId, amount);
            // 模拟网络异常:此处若服务宕机,A的余额被扣减,B的余额未增加,资金丢失
            // int a = 1 / 0;
            accountMapper.incrementBalance(toUserId, amount);
 
            return "转账成功!";
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return "转账失败,请重试!";
        } finally {
            // 5. 释放锁
            if (lock1.isHeldByCurrentThread()) {
                lock1.unlock();
            }
            if (lock2.isHeldByCurrentThread()) {
                lock2.unlock();
            }
        }
    }
}

2. Redis 锁在资金扣减场景的致命问题

3. 结论:资金扣减场景优先用 DB 锁

DB 锁结合事务的 ACID 特性,能保证核心数据的一致性和安全性,即使性能较低,也符合金融场景的核心需求(数据安全 > 性能)。

总结:Redis 锁与 DB 锁的选择指南

场景特征推荐使用原因
高并发、短事务、非核心数据Redis 锁性能高,能支撑高并发,少量异常可通过业务兜底解决
低并发、长事务、核心数据DB 锁(SELECT FOR UPDATE)事务 ACID 保障数据安全,无锁过期风险,满足合规审计要求
幂等性控制(如重复支付)DB 锁(唯一索引)无需锁等待,性能高于悲观锁,能有效防止重复写入
分布式系统、跨服务并发Redis 锁天然支持分布式,跨服务 / 跨机器的锁控制更简单
单服务、本地并发本地锁(synchronized)比 Redis 锁和 DB 锁更轻量,性能更高

补充:Redis 锁与 DB 锁的混合使用场景

在实际项目中,可结合两者的优势:

扩展补充:

一、为什么会有 Redis 锁的存在?为什么还会有 DB 锁的存在?

Redis 锁和 DB 锁的诞生,本质是不同业务场景对 “并发控制” 的需求存在本质差异,二者分别解决了对方无法高效解决的问题,是分布式系统中针对 “性能” 和 “数据安全” 的不同选择。

1. Redis 锁的诞生:为解决高并发分布式场景下的性能型并发控制问题

在 Redis 锁出现之前,处理分布式并发的方案有:

而 Redis 作为内存数据库,具备高性能、分布式部署、轻量易扩展的特性,基于它实现的分布式锁,恰好弥补了上述方案的缺陷:

总结:Redis 锁的存在,是为了满足分布式系统中高并发、短事务、非核心数据场景对 “高性能并发控制” 的需求,是性能优先的选择。

2. DB 锁的诞生:为解决核心数据场景下的安全型并发控制问题

在 DB 锁出现之前,仅靠应用层的逻辑控制并发,会面临以下问题:

而数据库作为持久化存储,具备事务 ACID 特性、数据持久化、锁与事务强绑定的特性,基于它实现的锁,恰好解决了上述问题:

总结:DB 锁的存在,是为了满足核心数据场景(资金、订单)、低并发、长事务对 “数据安全与一致性” 的需求,是安全优先的选择。

二、Redis 锁与 DB 锁最根本、最本质的区别

二者的本质区别,源于底层存储介质和设计目标的不同,最终体现为 **“性能与临时态” vs “安全与持久态”** 的核心差异:

维度本质特征(Redis 锁)本质特征(DB 锁)
存储介质内存(临时存储,非持久化优先)磁盘(持久化存储,数据落地优先)
设计目标高性能处理并发,牺牲部分数据安全的容错性高安全保障数据一致性,牺牲部分性能
锁的本质分布式协调的 “临时标记”:锁是内存中的一个 key-value,仅用于标记资源是否被占用,与数据操作无强绑定数据操作的 “原子性保障”:锁是事务的一部分,与数据操作强绑定,确保操作的 ACID 特性
一致性保障最终一致性(依赖应用层补偿机制,如重试、对账)强一致性(依赖数据库事务的 ACID 特性)
故障恢复锁状态可能丢失(如 Redis 宕机),需依赖过期时间、集群(Redlock)兜底锁状态与数据一起持久化,宕机恢复后状态不变

一句话总结本质区别:Redis 锁是基于内存的、松耦合的、性能导向的分布式并发协调工具;DB 锁是基于磁盘的、强耦合的、安全导向的事务内数据操作保障工具

三、Redis 锁解决的最核心问题?DB 锁解决的最核心问题?

1. Redis 锁解决的最核心问题

在分布式集群环境下,以极致的性能解决 “高并发场景中资源的并发访问控制” 问题,具体拆解为:

典型场景验证:秒杀活动中,Redis 锁能控制 10 万 + 并发请求对 100 台库存的访问,确保不超卖,且系统不会因并发压力崩溃 —— 这是 Redis 锁核心价值的体现。

2. DB 锁解决的最核心问题

在数据操作过程中,以事务的 ACID 特性解决 “核心数据的一致性与安全性保障” 问题,具体拆解为:

典型场景验证:银行转账场景中,DB 锁(SELECT FOR UPDATE)能保证用户 A 扣减 1 万元与用户 B 增加 1 万元的操作原子性,无论发生何种故障,都不会出现资金丢失或账实不符 —— 这是 DB 锁核心价值的体现。

补充:为何二者无法相互替代?

二者的共存,是分布式系统中 **“性能” 与 “安全” 平衡 ** 的必然结果。

到此这篇关于Redis锁与DB锁的使用与区别小结的文章就介绍到这了,更多相关Redis锁与DB锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文