日常开发中,针对一些需要锁定资源的操作,例如商城的订单超卖问题、订单重复提交问题等。
都是为了解决在资源有限的情况限制客户端的访问,对应的是限流。
目前针对这种锁资源的情况采取的往往是互斥锁,例如 java 里的 synchronized 锁以及 ReentrantLock,其中 synchronized 的加锁操作在 jvm 层面实现,会有一个锁升级(偏向锁、轻量级锁、重量级锁)的问题,ReentrantLock 需要手写代码实现,底层是 AQS。但是 java 层面的锁有一个问题,就是只能在一个进程中使用,如果跨进程就无能为力了,例如应用的集群部署,客户端请求过来后通过负载均衡策略转发到对应的实例上。
鉴于以上单节点锁的问题,就需要通过一个中间介质来实现针对需要访问的资源进行一个资源加锁和释放操作的问题,目前有如下方式
将需要访问的数据可以放到数据库的表中,一般存储的是确保唯一性的业务主键,在访问资源时可将业务主键插入到表中,每次访问资源前先查询数据库判断数据是否存在,如果存在表明资源在被访问,否则就正常处理。
https://zookeeper.apache.org/doc/r3.9.1/recipes.html#sc_recipes_Locks
通过临时顺序节点(EPHEMERAL_SEQUENTIAL )来实现,这种类型的节点的好处是会话级别,如果会话结束节点会删除掉。
官方提供的是 curator 组件库,在项目的 pom.xml 中引入如下依赖
org.apache.curator
curator-recipes
5.5.0
https://curator.apache.org/docs/getting-started#distributed-lock
内部提供了一个分布式锁接口 InterProcessLock,通过对应的实现类 InterProcessMutex 进行加锁和释放锁操作。
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessLock;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import java.util.concurrent.TimeUnit;
public class DistributedLock {
public static void main(String[] args) throws InterruptedException {
RetryPolicy retryPolicy = new ExponentialBackoffRetry(10000, 3);
for (int i = 1; i <= 1; i++) {
new Thread(() -> {
try (CuratorFramework client = CuratorFrameworkFactory.newClient(Constants.CONNECTION_URL, retryPolicy);){
client.start();
InterProcessLock lock1 = new InterProcessMutex(client, "/dlock");
try {
lock1.acquire(5, TimeUnit.SECONDS);
System.out.println("lock1获取");
TimeUnit.SECONDS.sleep(5);
lock1.release();
System.out.println("lock1释放");
} catch (Throwable e) {
e.printStackTrace();
}
}
}).start();
}
}
}
锁的组成如下
/父节点/_c_+UUID+-lock-+10位数(从0开始自增,不足10位用0补足)
如果在执行过程中进行了多次加锁,具体如下
/dlock/_c_0d4db7a9-5c21-4a57-9904-e42cd970d774-lock-0000000000
/dlock/_c_d3588ec1-981a-41aa-a420-89f7949f94d6-lock-0000000001
这样就会有一个问题,前面的前缀 PROTECTED_PREFIX 和 UUID 会对节点产生干扰。
https://github.com/apache/curator/blob/apache-curator-5.5.0/curator-framework/src/main/java/org/apache/curator/framework/imps/ProtectedUtils.java#L65
可以将 ProtectedUtils 整个类复制一遍在项目中,将 getProtectedPrefix() 的内容进行修改,如下
修改前
public static String getProtectedPrefix(final String protectedId)
{
return PROTECTED_PREFIX + protectedId + PROTECTED_SEPARATOR;
}
修改后
public static String getProtectedPrefix(final String protectedId)
{
return protectedId;
}
这样生成的节点格式为
/父节点/lock-+10位数(从0开始自增,不足10位用0补足)
最终生成的节点如下
/dlock/lock-0000000000
其中对应的节点值为当前请求的 ip
[zk: localhost:2181(CONNECTED) 2] get /dlock/lock-0000000000
192.168.106.109
如果有多个客户端针对同一个节点进行加锁请求,会按序创建多个节点,但是持有锁的只是最小的节点,后面的节点会向获取锁的节点注册 Watcher 来监听持有锁的节点是否存在。
作为一个在内存层次的数据库,用处多多,其中可以用于分布式锁。
redis 提供了 lua 脚本支持,lua 脚本可以做到将操作进行打包,确保整个操作的原子性。其中分布式锁就用到了 lua 脚本。
redis 官方介绍
https://redis.io/docs/manual/patterns/distributed-locks/
该锁被命名为 Redlock,在不同的语言中有对应的实现,在 java 中对应的是 redisson。
在项目的 pom.xml 中引入如下依赖
org.redisson
redisson
3.26.0
github 项目链接
https://github.com/redisson/redisson
与 spring boot 整合
https://github.com/redisson/redisson/tree/master/redisson-spring-boot-starter#spring-boot-starter
分布式锁建立在共享介质上,所以上面的三种方式都需要借助于其他组件来实现。
数据库层面不适合做高并发处理。
redis 依赖于全局时间,需要考虑到加锁时间的问题。
zookeeper 建立在创建节点的基础上,属于重操作,相对于 redis 内存操作慢一些。
由于 zookeeper 使用了 zab 协议,针对写请求会转发到领导者,如果领导者节点宕机,会从跟随者节点中选举出数据最完整的节点晋升为领导者,不会出现类似 redis 异步同步数据丢失的问题。
对于一些并发请求大的应用,使用 zookeeper 可能出现锁获取失败的情况,使用 redis 集群不会因为,因为 redis 的单线程高并发处理一般情况下难以达到,一般瓶颈在网卡带宽上。
之前自己写的文章
https://blog.csdn.net/zlpzlpzyd/article/details/132716450
参考链接
https://www.jianshu.com/p/31335efec309
https://www.cnblogs.com/xuwc/p/14019932.html
https://juejin.cn/post/6844903729406148622
https://www.cnblogs.com/crazymakercircle/p/14504520.html
https://blog.csdn.net/qq_26709459/article/details/112770526
https://zhuanlan.zhihu.com/p/639756647
https://zhuanlan.zhihu.com/p/383512946
https://www.cnblogs.com/zwj-199306231519/articles/17411947.html
https://baijiahao.baidu.com/s?id=1784900642230670850