1. 什么是锁?
在单进程的系统中,当存在多个线程可以同时改变某个变量时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
不同地方实现锁的方式也不一样,只要能满足所有线程都能看得到标记即可。如java中synchronize是在对象头设置标记,Lock接口的实现类基本上都只是某一个volitile修饰的int型变量其保证每个线程都能拥有对该int的可见性和原子修改,linux内核中也是利用互斥量或信号量等内存数据做标记。
2. 什么是分布式锁?
分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
3. 分布式锁实现的几种方式(每种方式都具备多种实现方案)
- 基于数据库实现分布式锁
基于数据库乐观锁实现:
乐观锁通常实现基于数据版本号(version)的记录机制实现的,在修改数据库前获取版本号,修改数据时与获取版本号不一致则抛出异常(修改数据时切记对版本号进行+1)
- 基于缓存(redis…)实现分布式锁
使用redis的setnx()、get()、getset()方法,用于分布式锁
- setnx(lockkey, 当前时间+过期超时时间) ,如果返回1,则获取锁成功;如果返回0则没有获取到锁,转向2。
- get(lockkey)获取值oldExpireTime ,并将这个value值与当前的系统时间进行比较,如果小于当前系统时间,则认为这个锁已经超时,可以允许别的请求重新获取,转向3。
- 计算newExpireTime=当前时间+过期超时时间,然后getset(lockkey, newExpireTime) 会返回当前lockkey的值currentExpireTime。
- 判断currentExpireTime与oldExpireTime 是否相等,如果相等,说明当前getset设置成功,获取到了锁。如果不相等,说明这个锁又被别的请求获取走了,那么当前请求可以直接返回失败,或者继续重试。
- 在获取到锁之后,当前线程可以开始自己的业务处理,当处理完毕后,比较自己的处理时间和对于锁设置的超时时间,如果小于锁设置的超时时间,则直接执行delete释放锁;如果大于锁设置的超时时间,则不需要再锁进行处理(这样可以避免死锁)。
- 基于Zookeeper的分布式锁
利用临时节点与 watch 机制。每个锁占用一个普通节点 /lock,当需要获取锁时在 /lock 目录下创建一个临时节点,创建成功则表示获取锁成功,失败则 watch/lock 节点,有删除操作后再去争锁。临时节点好处在于当进程挂掉后能自动上锁的节点自动删除即取消锁
4. 推荐开源分布式锁lock-spring-boot-starter
基于redisson实现的spring boot starter分布式锁框架,实现了可重入锁、公平锁、联锁、红锁、读写锁等常用锁的方式,并支持集群模式下的redis。
为什么推荐此项目?
- 我参与项目的开发
- 高可用,使用简单
- 支持单机模式,集群模式,云托管模式,哨兵模式
- 支持参数加锁,对象属性上加锁,支持方法上加锁
引入maven依赖即可使用
<dependency>
<groupId>io.gitee.tooleek</groupId>
<artifactId>lock-spring-boot-starter</artifactId>
<version>1.1.0</version>
</dependency>