在单体应用时代,传统企业级 Java应用为了解决“高并发下多线程访问共享资源时出现数据不一致”的问题,通常是借助 JDK自身提供的关键字或者并发工具类Synchronized、Lock和RetreenLock等加以实现,这种访问控制机制业界普遍亲切地称之为“锁”。不可否认的是,此种方式在很长一段时间内确实能起到一定的作用,在如今一些轻量级、比较小型的单体应用中依然可以见到其踪影

分布式锁,也是一种锁机制,只不过是专门应对“分布式”的环境而出现的,它并不是一种全新的中间件或者组件,而只是一种机制,一种实现方式,甚至可以说是一种解决方案。它指的是在分布式部署的环境下,通过锁机制让多个客户端或者多个服务进程互斥地对共享资源进行访问,从而避免出现并发安全、数据不一致等问题。
图片说明
● 排他性:这一点跟单体应用时代加的“锁”是一个道理,即需要保证分布式部署、服务集群部署的环境下,被共享的资源如数据或者代码块在同一时间内只能被一台机器上的一个线程执行。
● 避免死锁:指的是当前线程获取到锁之后,经过一段有限的时间(该时间一般用于执行实际的业务逻辑),一定要被释放(正常情况或者异常情况下释放)。
● 高可用:指的是获取或释放锁的机制必须高可用而且性能极佳。
● 可重入:指的是该分布式锁最好是一把可重入锁,即当前机器的当前线程在彼时如果没有获取到锁,那么在等待一定的时间后一定要保证可以再被获取到。
● 公平锁(可选):这并非硬性的要求,指的是不同机器的不同线程在获取锁时最好保证几率是一样的,即应当保证来自不同机器的并发线程可以公平获取到锁。

鉴于这几点要求,目前业界也提供了多种可靠的方式实现分布式锁,其中就包括基于数据库级别的乐观锁、悲观锁、基于Redis的原子操作、基于ZooKeeper的互斥排他锁,以及基于开源框架Redisson的分布式锁
图片说明

● 基于数据库级别的乐观锁:主要是通过在查询、操作共享数据记录时带上一个标识字段version,通过version来控制每次对数据记录执行的更新操作。
图片说明
● 基于数据库级别的悲观锁:在这里以MySQL的InnoDB引擎为例,它主要是通过在查询共享的数据记录时加上For Update字眼,表示该共享的数据记录已经被当前线程锁住了(行级别锁、表级别锁),只有当该线程操作完成并提交事务之后,才会释放该锁,从而其他线程才能获取到该数据记录。
图片说明

● 基于Redis的原子操作:主要是通过Redis提供的原子操作SETNX与EXPIRE来实现。SETNX表示只有当Key在Redis不存在时才能设置成功,通常这个Key需要设计为与共享的资源有联系,用于间接地当作“锁”,并采用EXPIRE操作释放获取的锁。
而之所以Redis的操作命令可以实现分布式锁的功能,主要是得益于Redis的单线程机制。对于Redis而言,不管其外层应用系统并发了 N多个线程,当每个线程需要使用Redis的原子操作或命令时,是需要进行“排队等待”的,即在Redis的底层基础架构中,同一时刻、同一个部署节点只允许一个线程执行某种操作。

图片说明

● 基于ZooKeeper的互斥排它锁:这种机制主要是通过ZooKeeper在指定的标识字符串(通常这个标识字符串需要设计为跟共享资源有联系,即可以间接地当作“锁”)下维护一个临时有序的节点列表Node List,并保证同一时刻并发线程访问共享资源时只能有一个最小序号的节点(即代表获取到锁的线程),该节点对应的线程即可执行访问共享资源的操作。
,ZooKeeper实现分布式锁功能的核心流程在于创建“临时顺序节点ZNode”以及采用Watcher监听机制监听临时节点的增减,从而判断当前的线程能够成功获取到锁。
图片说明