终于搞懂了,悲观锁、乐观锁、分布式都在什么场景下使用?有什么技巧?不容易呀!

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 如何确保一个方法,或者一块代码在高并发情况下,同一时间只能被一个线程执行,单体应用可以使用并发处理相关的 API 进行控制,但单体应用架构演变为分布式微服务架构后,跨进程的实例部署,显然就没办法通过应用层锁的机制来控制并发了。

如何确保一个方法,或者一块代码在高并发情况下,同一时间只能被一个线程执行,单体应用可以使用并发处理相关的 API 进行控制,但单体应用架构演变为分布式微服务架构后,跨进程的实例部署,显然就没办法通过应用层锁的机制来控制并发了。

那么锁都有哪些类型,为什么要使用锁,锁的使用场景有哪些?

锁类别

不同的应用场景对锁的要求各不相同,我们先来看下锁都有哪些类别,这些锁之间有什么区别。

  • 悲观锁(synchronize)
  • Java 中的重量级锁 synchronize
  • 数据库行锁
  • 乐观锁
  • Java 中的轻量级锁 volatile 和 CAS
  • 数据库版本号
  • 分布式锁(Redis锁)

乐观锁

就好比说是你是一个生活态度乐观积极向上的人,总是往最好的情况去想,比如你每次去获取共享数据的时候会认为别人不会修改,所以不会上锁,但是在更新的时候你会判断这期间有没有人去更新这个数据。

关注公众号:程序员白楠楠, 即可获取2020年末总结Java面试资料题一套。

乐观锁使用在前,判断在后。我们看下伪代码:

reduce()
{
    select total_amount from table_1
    if(total_amount < amount ){
          return failed.  
    }  
    //其他业务逻辑
    update total_amount = total_amount - amount where total_amount > amount; }

image.gif

  • 数据库的版本号属于乐观锁;
  • 通过CAS算法实现的类属于乐观锁。

悲观锁

悲观锁是怎么理解呢?相对乐观锁刚好反过来,总是假设最坏的情况,假设你每次拿数据的时候会被其他人修改,所以你在每次共享数据的时候会对他加一把锁,等你使用完了再释放锁,再给别人使用数据。

悲观锁判断在前,使用在后。我们也看下伪代码:

reduce()
{
    //其他业务逻辑
    int num = update total_amount = total_amount - amount where total_amount > amount; 
   if(num ==1 ){
          //业务逻辑.  
    } 
}

image.gif

  • Java中的的synchronize是重量级锁 ,属于悲观锁;
  • 数据库行锁属于悲观锁;

扣减操作案例

这里举一个非常常见的例子,在高并发情况下余额扣减,或者类似商品库存扣减,也可以是资金账户的余额扣减。扣减操作会发生什么问题呢?很容易可以看到,可能会发生的问题是扣减导致的超卖,也就是扣减成了负数。

举个例子,比如我的库存数据只有100个。并发情况下第1笔请求卖出100个,第2批卖出100元,导致当前的库存数量为负数。遇到这种场景应该如何破解呢?这里列举四种方案。

方案1:同步排它锁

这时候很容易想到最简单的方案:同步排它锁(synchronize)。但是排他锁的缺点很明显:

  • 其中一个缺点是,线程串行导致的性能问题,性能消耗比较大。
  • 另一个缺点是无法解决分布式部署情况下跨进程问题;

方案2:数据库行锁

第二我们可能会想到,那用数据库行锁来锁住这条数据,这种方案相比排它锁解决了跨进程的问题,但是依然有缺点。

  • 其中一个缺点就是性能问题,在数据库层面会一直阻塞,直到事务提交,这里也是串行执行;
  • 第二个需要注意设置事务的隔离级别是Read Committed,否则并发情况下,另外的事务无法看到提交的数据,依然会导致超卖问题;
  • 缺点三是容易打满数据库连接,如果事务中有第三方接口交互(存在超时的可能性),会导致这个事务的连接一直阻塞,打满数据库连接。
  • 最后一个缺点,容易产生交叉死锁,如果多个业务的加锁控制不好,就会发生AB两条记录的交叉死锁。

方案3:redis分布式锁

前面的方案本质上是把数据库当作分布式锁来使用,所以同样的道理,redis,zookeeper都相当于数据库的一种锁,其实当遇到加锁问题,代码本身无论是synchronize或者各种lock使用起来都比较复杂,所以思路是把代码处理一致性的问难题交给一个能够帮助你处理一致性的问题的专业组件,比如数据库,比如redis,比如zookeeper等。

这里我们分析下分布式锁的优缺点:

  • 优点:
  • 可以避免大量对数据库排他锁的征用,提高系统的响应能力;
  • 缺点:
  • 设置锁和设置超时时间的原子性;
  • 不设置超时时间的缺点;
  • 服务宕机或线程阻塞超时的情况;
  • 超时时间设置不合理的情况;

加锁和过期设置的原子性

redis加锁的命令setnx,设置锁的过期时间是expire,解锁的命令是del,但是2.6.12之前的版本中,加锁和设置锁过期命令是两个操作,不具备原子性。如果setnx设置完key-value之后,还没有来得及使用expire来设置过期时间,当前线程挂掉了或者线程阻塞,会导致当前线程设置的key一直有效,后续的线程无法正常使用setnx获取锁,导致死锁。

针对这个问题,redis2.6.12以上的版本增加了可选的参数,可以在加锁的同时设置key的过期时间,保证了加锁和过期操作原子性的。

但是,即使解决了原子性的问题,业务上同样会遇到一些极端的问题,比如分布式环境下,A获取到了锁之后,因为线程A的业务代码耗时过长,导致锁的超时时间,锁自动失效。后续线程B就意外的持有了锁,之后线程A再次恢复执行,直接用del命令释放锁,这样就错误的将线程B同样Key的锁误删除了。代码耗时过长还是比较常见的场景,假如你的代码中有外部通讯接口调用,就容易产生这样的场景。

设置合理的时长

刚才讲到的线程超时阻塞的情况,那么如果不设置时长呢,当然也不行,如果线程持有锁的过程中突然服务宕机了,这样锁就永远无法失效了。同样的也存在锁超时时间设置是否合理的问题,如果设置所持有时间过长会影响性能,如果设置时间过短,有可能业务阻塞没有处理完成,是否可以合理的设置锁的时间?

续命锁

这是一个很不容易解决的问题,不过有一个办法能解决这个问题,那就是续命锁,我们可以先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间之后重新去设置这个锁的超时时间,续命锁的实现过程就是写一个守护线程,然后去判断对象锁的情况,快失效的时候,再次进行重新加锁,但是一定要判断锁的对象是同一个,不能乱续。

同样,主线程业务执行完了,守护线程也需要销毁,避免资源浪费,使用续命锁的方案相对比较而言更复杂,所以如果业务比较简单,可以根据经验类比,合理的设置锁的超时时间就行。

方案4:数据库乐观锁

数据库乐观锁加锁的一个原则就是尽量想办法减少锁的范围。锁的范围越大,性能越差,数据库的锁就是把锁的范围减小到了最小。我们看下面的伪代码

reduce()
{
    select total_amount from table_1
    if(total_amount < amount ){
          return failed.  
    }  
    //其他业务逻辑
    update total_amount = total_amount - amount;  
}

image.gif

我们可以看到修改前的代码是没有where条件的。修改后,再加where条件判断:总库存大于将被扣减的库存。

update total_amount = total_amount - amount where total_amount > amount

image.gif

如果更新条数返回0,说明在执行过程中被其他线程抢先执行扣减,并且避免了扣减为负数。

但是这种方案还会涉及一个问题,如果在之前的update代码中,以及其他的业务逻辑中还有一些其他的数据库写操作的话,那这部分数据如何回滚呢?

我的建议是这样的,你可以选择下面这两种写法:

  • 利用事务回滚写法:

我们先给业务方法增加事务,方法在扣减库存影响条数为零的时候扔出一个异常,这样对他之前的业务代码也会回滚。

reduce()
{
    select total_amount from table_1
    if(total_amount < amount ){
          return failed.  
    }  
    //其他业务逻辑
    int num = update total_amount = total_amount - amount where total_amount > amount;   if(num==0) throw Exception;}

image.gif

  • 第二种写法

    reduce() { //其他业务逻辑 int num = update total_amount = total_amount - amount where total_amount > amount; if(num ==1 ){ //业务逻辑.
    } else{ throw Exception; } }
    

首先执行update业务逻辑,如果执行成功了再去执行逻辑操作,这种方案是我相对比较建议的方案。在并发情况下对共享资源扣减操作可以使用这种方法,但是这里需要引出一个问题,比如说万一其他业务逻辑中的业务,因为特殊原因失败了该怎么办呢?比如说在扣减过程中服务OOM了怎么办?

我只能说这些非常极端的情况,比如突然宕机中间数据都丢了,这种极少数的情况下只能人工介入,如果所有的极端情况都考虑到,也不现实。我们讨论的重点是并发情况下,共享资源的操作如何加锁的问题。

总结

最后我来给你总结一下,如果你可以非常熟练的解决这类问题,第一时间肯定想到的是:数据库版本号解决方案或者分布式锁的解决方案;但是如果你是一个初学者,相信你一定会第一时间考虑到Java中提供的同步锁或者数据库行锁。
小编总结了2020面试题,这份面试题的包含的模块分为19个模块,分别是: Java 基础、容器、多线程、反射、对象拷贝、Java Web 、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、MyBatis、RabbitMQ、Kafka、Zookeeper、MySQL、Redis、JVM 。
关注公众号:程序员白楠楠,获取上述资料。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
4月前
|
SQL 关系型数据库 MySQL
乐观锁在分布式数据库中如何与事务隔离级别结合使用
乐观锁在分布式数据库中如何与事务隔离级别结合使用
|
2月前
|
SQL 关系型数据库 MySQL
乐观锁在分布式数据库中如何与事务隔离级别结合使用
乐观锁在分布式数据库中如何与事务隔离级别结合使用
|
8天前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
40 10
|
17天前
|
调度 数据库
什么场景下要使用分布式锁
分布式锁用于确保多节点环境下的资源互斥访问、避免重复操作、控制并发流量、防止竞态条件及任务调度协调,常见于防止超卖等问题。
30 4
|
21天前
|
NoSQL Java 数据处理
基于Redis海量数据场景分布式ID架构实践
【11月更文挑战第30天】在现代分布式系统中,生成全局唯一的ID是一个常见且重要的需求。在微服务架构中,各个服务可能需要生成唯一标识符,如用户ID、订单ID等。传统的自增ID已经无法满足在集群环境下保持唯一性的要求,而分布式ID解决方案能够确保即使在多个实例间也能生成全局唯一的标识符。本文将深入探讨如何利用Redis实现分布式ID生成,并通过Java语言展示多个示例,同时分析每个实践方案的优缺点。
39 8
|
5月前
|
SQL 监控 NoSQL
乐观锁在分布式系统中如何实现
乐观锁在分布式系统中如何实现
|
2月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
71 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
2月前
|
NoSQL Java Redis
京东双十一高并发场景下的分布式锁性能优化
【10月更文挑战第20天】在电商领域,尤其是像京东双十一这样的大促活动,系统需要处理极高的并发请求。这些请求往往涉及库存的查询和更新,如果处理不当,很容易出现库存超卖、数据不一致等问题。
64 1
|
2月前
|
存储 缓存 NoSQL
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
73 4
|
2月前
|
SQL 监控 NoSQL
乐观锁在分布式系统中如何实现
乐观锁在分布式系统中如何实现
下一篇
DataWorks