分布式锁面试官问题
- zookeeper实现过分布式锁吗?
- 说说你用redis实现过分布式锁吗?如何实现的?
- 分布式情况下,如何解决高并发订单号重复问题
什么是分布式锁
集群环境下保证ID不重复
原来的Synchronized + Lock只能锁单机,也就是只能在一个JVM环境下
1:N关系
表示以前单机情况下,1个jvm虚拟机跑N个订单线程(这是个例子),在并发下,可以保证只有一个订单线程,拿到一个订单号,其余给锁住了
而在分布式+集群的环境下,变成了N对N的关系,在并发的环境下,如果使用UUID或者自增ID,就可能出现ID重复的问题,因此在集群下的环境下,对JVM进行加锁,这就是分布式锁。
分布式锁: 对多个jvm,同一时间只运行一个jvm运行某个业务的进程,其余相同的进程被锁住
比如jvm相当于教室,现在6个教室,要求采集学生信息同一时间只能一个教室的去(分布式锁)
分布式锁设计思想
针对小厂:qps < 2000
方案1:mysql的乐观锁 (了解即可,实际不可能)
方案2:redis(redission)
方案3:zookeeper
Zookeeper实现分布式锁
因为Zookeeper在创建节点的时候,需要保证节点的唯一性,也就是实现原理就是,每次一个线程获取到了锁,那就在Zookeeper上创建一个临时节点,但用完锁之后,在把这个节点删除掉
create /node v0410 # 创建一个持久节点
crate -e /node v0410 # 创建一个临时节点
对于单进程的并发场景,我们可以使用synchronized关键字和Reentrantlock等
对于 分布式场景,我们可以使用分布式锁。
创建锁
多个JVM服务器之间,同时在zookeeper上创建相同一个临时节点,因为临时节点路径是保证唯一,只要谁能创建节点成功,谁就能获取到锁。
没有创建成功节点,只能注册个监听器监听这个锁并进行等待,当释放锁的时候,采用事件通知给其它客户端重新获取锁的资源。
这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入获取锁的步骤。
释放锁
Zookeeper使用直接关闭临时节点session会话连接,因为临时节点生命周期与session会话绑定在一块,如果session会话连接关闭,该临时节点也会被删除,这时候客户端使用事件监听,如果该临时节点被删除的话,重新进入到获取锁的步骤。
zookeeper单机环境下的锁
假设我们现在有一个订单ID生成的工具类
/**
* Created by gysui on 2020/11/23
*/
public class OrderNumberCreateUtil {
private static int num = 0;
public String getOrderNumber() {
return "\t 生成订单号:" + (++num);
}
}
然后有一个OrderService服务类
/**
* Created by gysui on 2020/11/23
*/
public class OrderServiceImpl implements OrderService {
private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();
public void getOrderNumber() {
System.out.println(Thread.currentThread().getName() +
orderNumberCreateUtil.getOrderNumber());
}
}
当我们客户端有50个线程进行访问获取订单号的时候
0 生成订单号:1
10 生成订单号:10
9 生成订单号:9
6 生成订单号:8
8 生成订单号:7
7 生成订单号:6
5 生成订单号:5
1 生成订单号:1
3 生成订单号:3
4 生成订单号:4
2 生成订单号:2
15 生成订单号:15
14 生成订单号:14
13 生成订单号:13
12 生成订单号:12
11 生成订单号:11
16 生成订单号:16
18 生成订单号:17
17 生成订单号:18
19 生成订单号:19
20 生成订单号:20
21 生成订单号:21
22 生成订单号:22
23 生成订单号:23
24 生成订单号:24
25 生成订单号:25
26 生成订单号:26
27 生成订单号:27
28 生成订单号:28
29 生成订单号:29
30 生成订单号:30
31 生成订单号:31
32 生成订单号:32
33 生成订单号:33
34 生成订单号:34
35 生成订单号:35
36 生成订单号:36
38 生成订单号:37
39 生成订单号:38
42 生成订单号:39
37 生成订单号:40
43 生成订单号:41
41 生成订单号:42
44 生成订单号:44
40 生成订单号:43
46 生成订单号:45
49 生成订单号:49
47 生成订单号:48
48 生成订单号:47
45 生成订单号:46
解决方案是,在Service下,加锁解决
/**
* Created by gysui on 2020/11/23
*/
public class OrderServiceImpl implements OrderService {
private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();
public void getOrderNumber() {
Lock lock = new ReentrantLock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() +
orderNumberCreateUtil.getOrderNumber());
} finally {
lock.unlock();
}
}
}
分布式环境下的锁
模板模式
概念
在模板模式(Template Pattern)设计模式中,用一个抽象类公开定义了执行它的方法的方式、模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构,即可重定义该算法的某些特定步骤:
主要解决:一些方法通用,却在每个子类都重新写了这一方法
何时使用:在一些通用的方法
如何解决:将这些通用算法抽象出来
关键代码:在抽象父类中实现通用方法,其它步骤下放到子类中实现
应用实例
西游记里面菩萨定义好了81难,不管是女儿国,或者蜘蛛精,只需要有81劫难,这就是一个顶层的逻辑骨架
spring中对Hibernate的支持,将一些定好的方法封装起来,比如开启事务,获取Session,关闭Session,程序要不需要重复写那些已经规范好的代码,直接丢一个实体就可以保存。
优缺点
- 封装不变部分,扩展可变部分
- 提取公共代码,便于维护
- 行为由父类控制,子类实现
- 缺点是:每一个不同的实现,都需要一个子类来实现,导致类的个数增加,使得系统变庞大
使用场景
- 有很多子类共有的方法,且逻辑相同
- 重要的、复杂的方法,可以考虑模板方法
代码
例如,我们首先定义一个ILock的接口
/**
* Created by gysui on 2020/11/23
*/
public interface ILock {
/**
* 上锁,
* 首先会尝试加锁,如果加锁失败说明当前锁被占用
* 开始等待锁释放,如果一旦检测到锁被释放就递归该方法,再次尝试加锁
*/
void lock();
/**
* 解锁
*/
void unlock();
}
然后在抽象类中继承该接口,同时实现Lock 和 unLock的方法
/**
* Created by gysui on 2020/11/23
* 锁的模板类
*/
public abstract class LockTemplate implements ILock {
@Override
public void lock() {
// 尝试加锁
if (tryLock()) {
System.out.println(Thread.currentThread().getName() + "获取到锁");
} else {
// 等待释放锁
waitLock();
// 递归 lock
lock();
}
}
@Override
public void unlock() {
close();
}
/**
* 释放锁
*/
public abstract void close();
/**
* 尝试加锁
*/
protected abstract boolean tryLock();
/**
* 等待锁被释放
*/
protected abstract void waitLock();
}
同时我们在抽象类里,又定义了3个抽象方法,waitLock() 和 tryLock 以及clone()
/**
* Created by gysui on 2020/11/23
*/
public class ZkLock extends LockTemplate {
protected ZkClient zkClient = new ZkClient("192.168.1.106:2181",6000);
protected static final String PATH = "/lock";
public ZkLock() {
}
@Override
public void close() {
zkClient.close();
}
@Override
protected boolean tryLock() {
// 判断节点是否存在,如果存在则返回false,否者返回true
try {
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
return false;
}
}
@Override
protected void waitLock() {
CountDownLatch countDownLatch = new CountDownLatch(1);
IZkDataListener zkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
// 监控到锁被释放
countDownLatch.countDown();
}
};
// 等待锁的时候,需要加监控,查询这个lock是否释放
zkClient.subscribeDataChanges(PATH,zkDataListener);
try {
// 程序会阻塞在该处,直到 1 减少到 0
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
然后我们通过ILock进行加锁
/**
* Created by gysui on 2020/11/23
*/
public class OrderServiceImpl implements OrderService {
private OrderNumberCreateUtil orderNumberCreateUtil = new OrderNumberCreateUtil();
public void getOrderNumber() {
ILock lock = new ZkLock();
try {
lock.lock();
System.out.println(Thread.currentThread().getName() +
orderNumberCreateUtil.getOrderNumber());
} finally {
lock.unlock();
}
}
}
然后在使用多个线程进行操作,而且是在线程里面实例化对象,来进行创建,最终保证每个对象再获取订单的时候,都是唯一的
/**
* Created by gysui on 2020/11/23
* 分布式锁
*/
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
new Thread(() -> {
new OrderServiceImpl().getOrderNumber();
},String.valueOf(i)).start();
}
}
}
运行结果
44获取到锁
44 生成订单号:1
46获取到锁
46 生成订单号:2
39获取到锁
39 生成订单号:3
40获取到锁
40 生成订单号:4
17获取到锁
17 生成订单号:5
6获取到锁
6 生成订单号:6
29获取到锁
29 生成订单号:7
13获取到锁
13 生成订单号:8
0获取到锁
0 生成订单号:9
28获取到锁
28 生成订单号:10
36获取到锁
36 生成订单号:11
34获取到锁
34 生成订单号:12
31获取到锁
31 生成订单号:13
10获取到锁
10 生成订单号:14
2获取到锁
2 生成订单号:15
21获取到锁
21 生成订单号:16
26获取到锁
26 生成订单号:17
38获取到锁
38 生成订单号:18
32获取到锁
32 生成订单号:19
35获取到锁
35 生成订单号:20
43获取到锁
43 生成订单号:21
23获取到锁
23 生成订单号:22
9获取到锁
9 生成订单号:23
12获取到锁
12 生成订单号:24
47获取到锁
47 生成订单号:25
49获取到锁
49 生成订单号:26
41获取到锁
41 生成订单号:27
14获取到锁
14 生成订单号:28
8获取到锁
8 生成订单号:29
42获取到锁
42 生成订单号:30
18获取到锁
18 生成订单号:31
37获取到锁
37 生成订单号:32
30获取到锁
30 生成订单号:33
48获取到锁
48 生成订单号:34
16获取到锁
16 生成订单号:35
33获取到锁
33 生成订单号:36
25获取到锁
25 生成订单号:37
3获取到锁
3 生成订单号:38
7获取到锁
7 生成订单号:39
4获取到锁
4 生成订单号:40
20获取到锁
20 生成订单号:41
1获取到锁
1 生成订单号:42
11获取到锁
11 生成订单号:43
15获取到锁
15 生成订单号:44
19获取到锁
19 生成订单号:45
27获取到锁
27 生成订单号:46
22获取到锁
22 生成订单号:47
45获取到锁
45 生成订单号:48
24获取到锁
24 生成订单号:49
5获取到锁
5 生成订单号:50