悲观锁、乐观锁以及分布式锁

文章介绍了悲观锁和乐观锁的概念及其在数据库中的实现方式,悲观锁在读取数据时加锁,防止其他事务修改,而乐观锁则在更新时检查是否有并发修改。分布式环境下,可以通过数据库版本号实现乐观锁,或使用如Redis的锁机制和ZooKeeper的临时有序节点实现分布式锁。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.单机应用乐观锁悲观锁,select 时怎么加排它锁?

1.1悲观锁(Pessimistic Lock):

悲观锁特点:先获取锁,再进行业务操作。

即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁。当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的select for update如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。

补充:

不同的数据库对select for update的实现和支持都是有所区别的,

oracle支持select for update no wait,表示如果拿不到锁立刻报错,而不是等待,MySQL就没有no wait这个选项。

MySQL还有个问题是select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描。

1.2乐观锁(Optimistic Lock):

1.2.1乐观锁,也叫乐观并发控制,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,那么当前正在提交的事务会进行回滚。

1.2.2乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好。

乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持。

1.2.3 一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,

1.3 实现方式举例如下:

乐观锁(给表加一个版本号字段) 这个并不是乐观锁的定义,给表加版本号,是数据库实现乐观锁的一种方式。

SELECT data AS old_data, version AS old_version FROM …;

根据获取的数据进行业务操作,得到new_data和new_version

UPDATE SET data = new_data, version = new_version WHERE version = old_version

if (updated row > 0) {

// 乐观锁获取成功,操作完成

} else {

// 乐观锁获取失败,回滚并重试

}

注意:

乐观锁在不发生取锁失败的情况下开销比悲观锁小,但是一旦发生失败回滚开销则比较大,因此适合用在取锁失败概率比较小的场景,可以提升系统并发性能

乐观锁还适用于一些比较特殊的场景,例如在业务操作过程中无法和数据库保持连接等悲观锁无法适用的地方。

1.4 总结:

悲观锁和乐观锁是数据库用来保证数据并发安全防止更新丢失的两种方法,例子在select ... for update前加个事务就可以防止更新丢失。悲观锁和乐观锁大部分场景下差异不大,一些独特场景下有一些差别,一般我们可以从如下几个方面来判断。

响应速度: 如果需要非常高的响应速度,建议采用乐观锁方案,成功就执行,不成功就失败,不需要等待其他并发去释放锁。'

冲突频率: 如果冲突频率非常高,建议采用悲观锁,保证成功率,如果冲突频率大,乐观锁会需要多次重试才能成功,代价比较大。

重试代价: 如果重试代价大,建议采用悲观锁。

2. 分布式锁

2.1基于数据库实现分布式锁:也分为乐观锁和悲观锁(悲观锁也叫排他锁).

2.1.1分布式乐观锁实现方式:

在数据库表中引入一个版本号(version)字段来实现的。

当我们要从数据库中读取数据的时候,同时把这个version字段也读出来,如果要对读出来的数据进行更新后写回数据库,则需要将version加1,同时将新的数据与新的version更新到数据表中,且必须在更新的时候同时检查目前数据库里version值是不是之前的那个version,如果是,则正常更新。如果不是,则更新失败,说明在这个过程中有其它的进程去更新过数据了。

场景:假设同一个账户,用户A和用户B都要去进行取款操作,账户的原始余额是2000,用户A要去取1500,用户B要去取1000,如果没有锁机制的话,在并发的情况下,可能会出现余额同时被扣1500和1000,导致最终余额的不正确甚至是负数。但如果这里用到乐观锁机制,当两个用户去数据库中读取余额的时候,除了读取到2000余额以外,还读取了当前的版本号version=1,等用户A或用户B去修改数据库余额的时候,无论谁先操作,都会将版本号加1,即version=2,那么另外一个用户去更新的时候就发现版本号不对,已经变成2了,不是当初读出来时候的1,那么本次更新失败,就得重新去读取最新的数据库余额。

使用乐观锁满足的条件:锁服务要有递增的版本号version,每次更新数据的时候都必须先判断版本号对不对,然后再写入新的版本号

2.1.2分布式悲观锁实现方式:

基于 for update 来实现加锁

当加上排它锁之后,其它线程是无法操作这条记录的。那么,这样的话,我们就可以认为获得了排它锁的这个线程是拥有了分布式锁,然后就可以执行我们想要做的业务逻辑,当逻辑完成之后,再调用上述释放锁的语句即可。

基于redis缓存实现分布式锁

a.主要是依赖redis自身的原子操作;

b. set user_key user_value NX PX100:redis从2.6.12版本开始,set命令才支持这些参数

nx : 只在在键不存在时,才对键进行设置操作,SET key value NX 效果等同于 SETNX key value;

px毫秒: 设置键的过期时间为millisecond毫秒,当超过这个时间后,设置的键会自动失效

这段命令理解:当redis中不存在user_key的时候,才会去设置一个user_key,并且这个键的值为user_value,且这个键的寻获时间为100ms

这个命令是只有在某个key不存在的时候,才会执行成功。那么当多个进程同时并发的去设置同一个key的时候,就永远只会有一个进程成功。当某个进程设置成功之后,就可以去执行业务逻辑了,等业务逻辑执行完毕之后,再去进行解锁。解锁很简单,只需要删除这个key就可以了,不过删除之前需要判断,这个key对应的value是当初自己设置的那个。

c. redis集群模式的分布式锁,可以采用redis的Redlock机制。

基于Zookeeper实现分布式锁

基于ZooKeeper,就是使用它的临时有序节点来实现的分布式锁。

原理:当某客户端要进行逻辑的加锁时,就在zookeeper上的某个指定节点的目录下,去生成一个唯一的临时有序节点, 然后判断自己是否是这些有序节点中序号最小的一个,如果是,则算是获取了锁。如果不是,则说明没有获取到锁,那么就需要在序列中找到比自己小的那个节点,并对其调用exist()方法,对其注册事件监听,当监听到这个节点被删除了,那就再去判断一次自己当初创建的节点是否变成了序列中最小的。如果是,则获取锁,如果不是,则重复上述步骤。当释放锁的时候,只需将这个临时节点删除即可

<think>好的,我现在需要回答用户关于分布式锁乐观锁悲观锁的概念、区别、原理及使用场景的问题。首先,我得回忆一下这三种锁的基本定义,然后根据提供的引用资料来整理信息,确保准确性和全面性。 首先,悲观锁。根据引用[1][2][4],悲观锁的核心思想是假设最坏的情况,每次操作数据时都认为会有其他并发操作,因此先加锁再操作。数据库中的SELECT FOR UPDATE就是典型例子,适用于写操作频繁的场景,比如账户余额更新,避免数据冲突,但可能降低并发性能。 然后是乐观锁,引用[1][3][4]提到,乐观锁假设冲突较少,通过版本号或时间戳机制实现,比如CAS操作。适用于读多写少的情况,比如库存扣减,先操作后检查版本,减少了锁的开销,但需要处理更新失败的情况。 分布式锁,引用[1][3][4]指出,用于分布式系统中跨进程的资源控制,Redis的Redisson实现就是例子,支持可重入、自动续期,适用于秒杀系统或分布式任务调度,相比数据库锁性能更好,但需要处理网络延迟等问题。 接下来是区别部分。原理上,悲观锁依赖数据库锁,乐观锁用版本号,分布式锁用外部存储如Redis。使用场景方面,根据引用[3][4],悲观锁适合高并发写,乐观锁适合读多写少,分布式锁适合跨系统。性能上,乐观锁分布式锁通常比悲观锁高效,但各有适用条件。 需要确保回答结构清晰,分点说明,并引用相关资料。同时要生成相关问题,可能涉及应用场景选择、实现方式对比等。最后检查引用标识是否正确,避免遗漏。可能还需要注意用户提供的引用中的一些细节,比如Redisson的具体功能,以及不同锁的适用场景对比。</think>### 分布式锁乐观锁悲观锁详解 #### 一、基本概念与原理 1. **悲观锁** - **原理**:假设并发操作必然发生冲突,通过**先加锁后操作**的方式确保数据独占性。例如数据库的 `SELECT ... FOR UPDATE` 语句会直接锁定记录[^2][^4]。 - **实现**:依赖数据库锁机制(如行锁、表锁),或编程语言的互斥锁(如Java的 `synchronized`)。 2. **乐观锁** - **原理**:假设冲突概率低,通过**版本控制**(如时间戳、版本号)实现无锁化操作。例如更新数据时检查版本是否匹配,若冲突则重试或回滚[^3][^4]。 - **实现**:常用CAS(Compare And Swap)算法,如MySQL的 `UPDATE table SET value=new_value, version=version+1 WHERE id=1 AND version=old_version`。 3. **分布式锁** - **原理**:在分布式系统中协调多个节点对共享资源的访问,通常基于**外部存储系统**(如Redis、ZooKeeper)实现锁的互斥性。例如Redisson的 `RLock` 通过Lua脚本保证原子性[^1][^3]。 - **实现**:Redis的 `SETNX` 指令、Redisson的可重入锁、ZooKeeper的临时有序节点。 --- #### 二、核心区别 | 维度 | 悲观锁 | 乐观锁 | 分布式锁 | |--------------|-------------------------|-------------------------|--------------------------| | **冲突假设** | 高概率冲突 | 低概率冲突 | 跨进程/节点冲突 | | **加锁时机** | 操作前加锁 | 操作后检查版本 | 通过外部系统协调加锁 | | **性能影响** | 高(阻塞其他操作) | 低(无锁,可能需重试) | 中等(依赖网络通信) | | **适用场景** | 写操作频繁(如转账)[^2] | 读多写少(如库存扣减)[^3] | 分布式任务调度、秒杀系统[^1] | --- #### 三、典型应用场景 1. **悲观锁** - 银行账户余额更新(避免并发扣款冲突)[^2]。 - 订单状态修改(确保同一订单不会被重复处理)。 2. **乐观锁** - 电商库存扣减(版本号防止超卖)[^3]。 - 文档协同编辑(通过时间戳检测内容冲突)。 3. **分布式锁** - 分布式定时任务(防止多节点重复执行)[^1]。 - 秒杀系统中商品抢购(Redis分布式锁控制并发请求)。 --- #### 四、选型建议 - **单体应用**:优先选择数据库悲观锁(简单可靠)[^3]。 - **高并发读场景**:使用乐观锁减少锁开销[^4]。 - **分布式系统**:Redis分布式锁(性能高,支持自动续期)或ZooKeeper(强一致性)[^1][^3]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值