现代计算机基本都是多核,而且我们的业务进程通常会运行在多台计算机上。不管是一台计算机上的多个线程还是运行在多台计算上的不同进程在处理系统资源时难免会出现冲突,为了解决共享资源
的冲突问题我们经常需要加锁处理。
举两个常见的例子。
例1:a=0,10个线程同时执行a ,在程序运行多次后会发现每次得到a的值是不确定的,如果想保证a最后的值是10,则可以通过加锁,把并发的线程串行化。
例2:某电商平台限量抢购活动,商品A只能抢购50件,如何保证最后只卖出去50件。我们也可以依赖锁机制,每次扣件库存都加锁。这里的加锁不一定必须是我们业务代码加锁,也可以是数据库的行锁或者依赖其它中间件的分布式锁。
锁的本质
结合上面的两个例子,说一说我对锁的理解。锁的本质是:业务场景中存在共享资源
,多个进程或线程需要竞争获取并处理共享资源,为了保证公平、可靠、结果正确等业务逻辑,要把并发执行的问题变为串行,串行时引入第三方锁
当成谁有权限来操作共享资源的判断依据。
这就成功把业务问题抽象成了取锁
问题。那在计算机中是如何处理锁问题的呢?
锁的实现
锁的实现最根本的还是需要硬件支持,CPU提供了原子操作的指令,比如在X86、ARM等架构中都提供了一些机制保证多条操作指令在一个原子操作中执行。这些原子操作是我们实现锁的基础。
操作系统对CPU提供的锁又进一步进行了封装,比如Linux操作系统就提供了自旋锁(spinlock)、互斥锁(mutex)、读写锁(dwlock)、RCU锁等一些常用的逻辑锁。
我们用来做业务开发的各种语言又在操作系统和汇编基础之上给我们提供了更加方便的、开箱即用的锁结构。比如Golang中的互斥锁(sync.Mutex)、读写锁(sync.RWMutex)、automic的各种原子操作。
锁的问题
我们可以把锁分为悲观锁和乐观锁两类。所谓悲观锁就是假定其它线程会产生共享资源的竞争,一定要先获取锁结构才执行后续的逻辑。乐观锁恰好相反,认为不一定会产生竞争,直接修改值如果成功了就达成目标,如果不成功再进行重试,直到成功或者超时。
CAS(compare and swap)是实现乐观锁的一种典型技术。在CAS机制中存在三个基本操作值,value内存值,old旧预期值,new预期值。
代码语言:javascript复制func compareAndSwap(value int, old int, new int) bool{
if (value !=old){
return False
}
*value= new
return True
}
如上面的代码,我们比较value和old是否相等,如果不相等说明old值已经被修改,取锁失败;如果相等则把new赋值给value,取锁成功。当取锁失败时,会修改value的值为当前值等待进行下一次取锁,直到超时或者取锁成功。
CAS有一个典型的问题:ABA。所谓ABA问题是指,在执行CAS操作时,其它并发执行的线程把内存值A修改为B,之后又修改为A,CAS机制认为A没有变化,其实A中间已经产生了变化状态,这就可能导致多个线程同时获取到锁。解决ABA问题的典型解决方案是增加版本号,Golang和Java语言中也都有相关实现。加版本号的具体方法是在并发执行的多个线程修改变量A时,增加一个递增的版本号,执行成功的条件需要版本号也保持一致,而且执行成功后版本号 1,这就保证了多个进程同时只能有一个获取到锁,也不会因为重试产生逻辑问题。
分布式锁
为了保证系统的高可用,很多进程都需要部署多个实例,为了能让多个实例正常处理共享资源,就没法在这些实例的内部用同一个锁,因此必须引入第三方系统来实现锁。我们可以依赖MySQL的行锁、Redis的原子操作等实现锁。
依赖第三方系统生成锁结构时,三方系统需要保障自己高可用。比如我们用单机的Redis来实现锁操作,单机的Redis如果宕机了就会造成所有进程取锁失败。为了保证Redis的高可用,我们可以采用主从的架构模式,当我们采用主从架构时会存在Master节点同步数据到Slave节点的时间延迟。如果AB两个进程,A在Master节点获取到锁结构之后,数据同步到Slave节点之前,B进程读Slave节点的数据发现没有此锁,也修改某个key的值获取到了锁,这就会导致AB同时获得到锁,这明显违背了使用锁的初衷。
Redis做分布式锁时也存在其它解决方案,我们只是举例说明问题。
综上我们依赖的三方系统需要保持其对外数据的一致性
,也就是我们通常所说的CAP理论中的C(Consistency)特性,基于此特性开源软件中ETCD和Zookeeper都是不错的选择。
在实际工作中是自研还是选择一种开源产品做分布式锁,最终还要看我们的业务场景,如果需要强一致的CP模型,那我们首选ETCD,如果可以存在一些不一致情况Redis也是不错的选择。当然还要结合业务的调用量、运维能力等综合考虑。
注意问题:锁的实现,不管是CAS还是其它实现方式都比较消耗CPU性能,所以如果能通过业务逻辑设计避免使用锁是最好的。 锁的力度越小越好。 Sharding是提高锁效率的一种有效手段。
总结
本文我们主要介绍了计算机中的锁,简单介绍了锁的实现方式,锁的常见问题和如何选择分布式锁。文中没有对具体一种实现方式做源码级的剖析,更多的是站在旁观者的角度思考锁问题的原理和本质。
以上纯属个人学习、总结的一些观点,欢迎大家私信交流。