使用分布式锁时考虑哪些问题

本文深入探讨了分布式锁的应用场景,如退款处理,以及在分布式环境中引入锁的原因和局限性。分析了分布式锁的三个核心要素:外部存储、全局唯一标识和两种状态,并进一步讨论了分布式锁的原子性、数据一致性、性能、可重入性、公平性和容错机制等进阶话题,特别强调了锁的超时与心跳机制在防止锁无法释放问题上的重要性。
摘要由CSDN通过智能技术生成

工作中经常会遇到争抢共享资源的场景,比如用户抢购秒杀商品,如果不对商品库存进行保护,可能会造成超卖的情况。超卖现象在售卖火车票的场景下更加明显,两个人购买到同一天同一辆列车,相同座位的情况是不允许出现的。交易系统中的退款同样如此,由于网络延迟和重复提交极端时间差的情况下,可能会造成同一个用户重复的退款请求。以上无论是超卖,还是重复退款,都是没有对需要保护的资源或业务进行完善的保护而造成的,从设计方面一定要避免这种情况的发生。

本文以退款交易场景入手,引入分布式锁,尝试分析分布式锁需要考虑关注点,包括以下内容:

  • 锁的引入和局限性
  • 分布式锁的三要素
  • 分布式锁进阶
    • 获取锁操作的原子性
    • 锁与保护共享资源的数据一致性
    • 分布式锁的性能
    • 可重入
    • 公平锁和非公平锁
  • 分布式锁的容错,使用分布式锁时注意考虑哪些问题

锁的引入和局限性

锁是一种控制共享资源争抢的机制,采用互斥方式防止多线程(或多进程)间造成的冲突。锁是一种获取保护资源的凭证,就像公园门票,只有持有门票才有资格入园;锁是使得对同一类共享资源的访问串行化。没有获得锁只能排队等待,直到其他线程释放掉锁。这里需要对“同一类共享资源”正确理解,比如订单系统中的同一种商品库存,退款系统中同一个用户。

在多线程中,Java 已经提供了很好原生锁(包括synchronized,lock),前面的其他文章中也已经讲到了内置锁和显示锁的理解和使用,在此不再赘述。但是(是不是已经料到了我要说但是了呢?),在分布式系统中,因为要跨进程或者跨服务器 ,这种场景下JDK原生锁已经无法满足我们的需求,需要一种能够分布式系统中保护共享资源的方式,分布式锁在这种情况下产生了。

很多事情往往都是如此,为了解决一个问题,引入了新方案,而新方案却会带来其他的问题,又需要用更多的时间去解决新方案带来的问题。没有一个完美的方案,因此对方案的取舍,就是具体场景中应该重点关注哪些问题,忽略哪些问题的选择。

分布式锁的三要素

分布式锁是一个在分布式环境中很重要的原语,它表明不同进程间采用互斥的方式操作共享资源。如何才称得上分布式锁呢?分布式锁需要满足三个基本的条件:

  1. 外部存储
  2. 全局唯一标识
  3. 至少有两种状态,获取和释放
  • 外部存储
    顾名思义,分布式锁是在分布式部署环境中给多个主机提供锁服务。Java具有天生的多线程优势,在同一个进程的线程中可以通过互斥锁住共享资源来保证多线程之间干扰,锁的载体是堆中共享变量,使用JDK原生锁synchronized和lock可以很方便的解决,但是将问题扩展到分布式环境中,就超出了JDK原生锁作用范畴。需要另外的存储载体,可以是共享内存或者磁盘文件。考虑到分布式锁的高可用性,避免单点问题,因此共享内存中数据是需要持久化的,这点内容会在下文中的分布式锁的高可用中涉及到。

  • 全局唯一标识
    与JDK原生锁类似,分布式锁同样需要标记为全局唯一。在多线程环境中,锁可以使一个对象引用,也可以是基本类型变量,都有唯一的标识来区分锁保护的不同资源。仍然以上面的退款为例,为了保护用户的账户资金,不允许同一个用户并发退款。因此同一个用户退款操作采用互斥锁保护起来,不同用户之间不需要互斥操作。具体方法一种可以通过锁用户账户的方式,另一种对用户userId设置不同的状态标识,这两种方式都是采用对堆中变量的原子操作保证互斥的。
    分布式环境中上述第一种方法就不适用了,举个例子,小明的账户可以同时在A、B两个不同实例中加锁。那么可以采用第二种方法,自定义一个标识,使其全局唯一即可,每次申请退款时,首先尝试获取该标识,如果该标识已经被其他占用,则需要等待,直到释放该标识(是不是与synchronized很相似)。对于交易而言,全局唯一的标识很简单:业务+userId即可唯一标识。

  • 至少有两种状态
    锁至少需要两种状态:加锁(lock)和解锁(unlock)。用状态区分当前尝试获取的锁是否已经被其他操作占用,被占用只有等待锁释放后才能尝试获取锁并加锁,保护共享资源。

分布式锁进阶

为解决共享资源在分布式环境下并发访问带来的问题,引入分布式锁采用互斥访问的方式将并发访问串行化。下文中以Redis为例,分析使用分布式锁时重点需要考虑的情况。

  • 获取锁操作的原子性
    从读取锁的状态,到设置锁状态为加锁(获取锁的过程),不是原子性的操作,如果不能保证这两步作为一个的原子操作,可能存在竞态条件,在极端的时间差的情况下,会有多个服务同时获取到同一个锁,从而获取操作工作资源的凭证,这是不允许的。幸运的是Redis提供了CAS原子性功能SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置。

  • 锁与保护共享资源的数据一致性
    获取锁与开始操作共享资源必须保证一致性,结束操作共享资源和释放锁必须保证一致性。共享资源操作结束后必须释放锁,退出临界区,否则会造成锁饥饿;开始操作共享资源,必须是在获取锁之后,否则锁就无法保护共享资源。

  • 分布式锁的性能
    分布式锁需要考虑网络传输时间,超时时间同样需要考虑网络时间消耗。

  • 可重入
    某个请求试图获得一个已经由它自己持有的锁,那么这个请求就会成功,这是重入。当重入时需要将计数器加一,释放锁时,计数器相应减一,一般分布式锁同样支持可重入,因此需要设计标记不同的请求。

  • 公平锁和非公平锁
    公平锁设定按照请求的顺序获取锁,不允许插队。公平是个好东西,不过大多数情况下非公平锁的性能要高于公平锁。

分布式锁的容错

正常情况下,加锁,执行保护资源,释放锁。如果没有异常,那这世界就太美好了。那么生产环境中,使用分布式锁时应该注意哪些容错的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值