大家好,又见面了,我是你们的朋友全栈君。
业务场景:
电商系统,用户下单,需要生成唯一的订单编号,并且需要有业务意义,而不能使用UUID这种字符串,比如:
年-月-日-时-分-秒-自增序号
2019-11-11-23-59-59-001
订单编号生成器工具类OrderCodeGenerator
/**
* @description: 订单编号生成器
* @author: stwen_gan
* @date:
**/
public class OrderCodeGenerator {
/** 自增序列 **/
private int i = 0;
/**
* 根据当前时间生成订单编号:"yyyy-MM-dd-HH-mm-ss-自增序号"
* @return
*/
public String getOrderCode(){
Date date = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-");
return simpleDateFormat.format(date)+ ++i;
}
}
创建订单服务
@Slf4j
public class OrderServiceImpl implements OrderService {
private OrderCodeGenerator codeGenerator = new OrderCodeGenerator();
/**
* 创建订单
* @return
*/
@Override
public String createOrder() {
//生成订单编号
String orderCode = codeGenerator.getOrderCode();
log.info(Thread.currentThread().getName()+"-->生成订单编号:{}",orderCode);
// TODO 具体写自己的生成订单业务
return orderCode;
}
}
一、单机环境
服务部署一个实例,并发情况,订单编号能唯一吗?
如何模拟并发:
模拟20个并发线程,使用for循环创建20个线程,使用CyclicBarrier .await() 并发协调控制,使20个线程全创建好之后,再同时一起启动往下执行!(循环屏障)
模拟并发测试 ConcurrentTest
@Slf4j
public class ConcurrentTest {
public static void main(String[] args) {
//并发线程数
int count = 20;
//循环屏障
CyclicBarrier cb = new CyclicBarrier(count);
OrderService orderService = new OrderServiceImpl();
//模拟高并发场景,创建订单
for(int i=0; i<count; i++){
new Thread(new Runnable() {
@Override
public void run() {
log.info(Thread.currentThread().getName()+"--我已经准备好了");
try {
//等待所有线程启动准备好,才一起往下执行
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
//创建订单
orderService.createOrder();
}
}).start();
}
}
}
运行该测试类main方法,控制台如下:
可以看到,模拟的20个人同时并发下单,生成的订单编号有重复:
解决方案一:在订单服务类,调用订单编号生成方法时,加上Lock锁(可重入锁),并发情况,只有获取到锁的才可以进去调用。
改进订单服务:
加了Lock锁优化后,变成如下场景:
再次运行刚才的测试类,生成的工单编号没有重复了。
但是假如,有上万的并发,单台服务无法支撑怎么办?
—加服务器,部署集群!!!
二、集群环境
订单服务部署集群,并发下单,此时订单编号能唯一吗?
场景如下:
做集群,如何模拟验证?
是不是要部署多台tomcat??
之前模拟并发测试类,订单服务是放在for外面,如下:
修改:将订单服务实例,移到创建线程的for循环里,每个线程都会创建调用一个订单服务,从而模拟集群场景(这里模拟20个线程,则模拟20个tomcat):
再次运行测试类,结果如下:
订单编号的末尾序号最后全是1,为什么?
–> 因为模拟了20个tomcat(每个线程分别创建了属于自己的orderService订单服务实例),每个都调用不同的编号生成实例,相当于每个tomcat都有一个编号生成器,每个都从1开始。如下:
再次改进方案:
将订单编号生成器类单独抽取一个tomcat,使多个tomcat调用同一个编号生成器服务
如何模拟验证?
在订单编号服务调用类中,将订单编号生成器类实例,设为static 静态变量,则他们调用的都是同一个实例。
此时订单编号能唯一吗?
把并发数收到50或100,尽量大点,看出效果:
为什么还会出现重复编号?
因为每个tomcat都有自己各自的锁,不是同一把锁!如下:
那我们再把锁也加上static,锁也是唯一实例了,可以吗?
刚才,我们上面模拟的只是,每个tomcat只分配一个订单请求的情况,如下场景:
–>还是不可以,因为,假如tomcat1有20个并发,它们用的的确是同一把锁(同一个jvm下,static锁),而tomcat2也有20个并发,它们用的却是另外一把锁…
所以,使用jvm锁,无法解决集群并发问题!!!!
再再改进方案:
我们把生成订单编号独立成共享服务,并且把锁加在编号服务上(把锁在共享编号服务后面),如下:
此时,并发情况,订单编号还会出现重复吗?
后面加锁,虽然订单编号不会重复了,但是,大并发情况,阻塞卡死在后面单台服务器(假如后面订单编号服务也部署集群,那么又会出现编号重复)。
因此,需要使用分布式锁!!!
终于进入本文的正题了 —>别走开,后面更精彩0.0
三、Zookeeper分布式锁
1、分布式锁,场景描述:
分布式锁用途:在分布式环境下协同共享资源的使用。
2、分布式锁思路分析
锁特点:
-
排他性:同一时间,只有一个线程能获得;
-
阻塞性:其它未抢到的线程阻塞等待,直到锁被释放,再继续抢;
-
可重入性:线程获得锁后,后续是否可重复获取该锁(避免死锁)。
当然,还要考虑性能开销等问题。
3、常规的分布式锁解决方案有哪几种:
-
文件系统:同一个目录下,不能存在同名文件
-
数据库锁:主键 、 唯一约束 、for update
-
基于Redis的分布式锁:setnx、set、Redisson
-
基于ZooKeeper的分布式锁:类似文件系统
对比分析:
-
使用数据库锁会有单机性能、单机故障等问题,锁没有失效时间,容易出现死锁,当然可以部署群集,也会出现各种各样的问题,性能开销高,这里不详细介绍。
-
Redis缓存实现分布式锁,相对复杂,因为没有类似zk的watch监听通知机制,需要自己另外实现;而且Redis可能会出现死锁(或短时间内死锁),比如,获取到锁的线程挂了,必须等到该节点过期时间到了,才能删除。
-
而Zookeeper分布式锁可靠性比Redis好,实现相对简单,但由于需要创建节点、删除节点等,所有效率相比Redis要低。
那我们在实际项目中如何选择呢?
原则上如果并发量不是特别大,追求可靠性,那么首选zookeeper。而Redis实现的分布式锁响应更快,对并发的支持性能更好,如果为了效率,首选redis实现。
由于篇幅原因,本次仅介绍Zookeeper分布式锁解决方案,下次再介绍redis分布式锁方案。
4、Zookeeper简介
ZooKeeper是一个开源的的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
zk特点:
-
类似文件目录的DataTree数据结构,可以在znode节点存取数据;内部是一个Map<String, DataNode> nodes的数据结构,其中key是path,DataNode才是真正保存数据的核心数据结构。
-
zk每个节点都有watcher监听通知机制,监听znode节点的增删改查的操作。
zk节点类型:
-
持久节点 (persistent):节点创建后,就一直存在,直到主动删除。
-
持久顺序节点 (persistent_sequential)
-
临时节点 (ephemeral):客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper服务端会自动删除该节点。
-
临时顺序节点 (ephemeral_sequential)
并且,同一个znode下,节点名称是唯一的。
5、zk服务安装:
官网下载zk包(windows),解压缩,如下:
演示注册watch监控节点变化:
(1)编写zk监听器测试类,双击运行zkServer.cmd,运行该类
(2)运行zkCli.cmd ,分别操作节点:/stwen/lock
控制台输出如下:
监听到该节点数据变化--->666
监听到该节点数据变化--->888
监听到该节点被删除--->/stwen/lock
6、zk分布式锁方案一
并发情况,都连接到ZK,先去ZK下创建子节点,
(1) 创建成功,则获得锁,用完释放锁,并通知其他订阅了该节点的线程;
(2) 假如创建节点失败,则注册该节点的watcher监听器(都去关注订阅那个节点),进入阻塞等待。
逻辑如下:
利用zk特性:同父的子节点不可以重名。
思考:
(1)用持久节点可以吗?
–>假如拥有锁的线程挂了呢?岂不是出现死锁…
(2)用临时节点呢?
–>使用临时节点,它有个特性,只要zk客户端与zk服务端失去连接,该节点就会被删除,那么其他注册了该节点watcher监听的线程则都会收到通知,去抢锁,不会死锁。
步骤:
引入ZKClient依赖
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.11</version>
</dependency>
-
创建一个自己的ZKDistributeLock,实现Lock接口,复写它的tryLock()、lock()、unlock()等方法;
-
tryLock()方法调用ZK的接口api,去给定节点下创建临时节点,创建成功,则获得锁,执行业务代码(去调用生成工单编号的服务),释放锁;
-
tryLock()中创建临时节点失败,则注册一个watcher监听该节点的变化,并进入阻塞等待,当有节点释放锁时,会通知订阅了该watcher的线程,取消watcher监听,并尝试重新去创建临时节点,去抢锁。
-
多线程情况,调用前,先创建一个ZKDistributeLock,先获取锁,才能调用生成工单编号。
具体代码实现这里就不粘贴了,这种情况是肯定不会出现订单编号重复的。
但是:
假如有1万的并发,当其中一个获取到锁之后,释放锁时,同时就要通知9999个监听者,造成网络冲击等(惊群效应),最后也只能是只有一个能获取到锁!!!
在集群规模较大的环境中带来危害:
-
巨大的服务器性能损耗
-
网络冲击
-
可能造成宕机
所以,终极方案:使用临时顺序节点实现zk分布式锁!
7、zk分布式锁方案二
使用临时顺序节点实现zk分布式锁,类似去银行办业务,取号排队(按顺序),上一个办完只会通知下一个。
逻辑如下:
步骤:
-
同样是创建一个自己的ZKDistributeLock,实现Lock接口,复写它的tryLock()、lock()、unlock()等方法;
-
tryLock()方法调用ZK的接口api,尝试去获取锁,在给定节点下创建临时顺序节点,并获取子节点列表,然后判断当前创建的子节点序号是否比其他子节点小,如果是最小,则获得锁并执行业务代码,释放锁;
-
tryLock()中如果对比其他子节点,自己创建的子节点并不是最小,则去对比自己小的节点注册watcher监听,只监听比自己上一个小的节点,进入阻塞等待。
-
unlock()当释放锁时,watcher监听器会实时通知监听了它的节点(比他大一号的节点),触发他又重新获取子节点列表,并对比是否最小。。。
具体代码实现:
* 利用zk临时顺序节点实现分布式锁
* 获取锁:取排队号(创建自己的临时顺序节点),然后判断是否是最小号,则获得锁,不是则注册前一个节点的监听watcher,阻塞等待
* 释放锁:删除自己创建的临时节点
(1)创建一个分布式锁类 ZkDistributeLock,实现Lock接口,构造方法中初始化ZK连接,并实现lock()、unlock()、tryLock()等方法:
实现lock()
实现tryLock()方法
其中,waitGetLock()方法,如下:
(2)新增一个订单服务实现类
(3)将测试类中的订单服务换成上面的DisLockOrderServiceImpl
(4)运行测试类,控制台打印如下,没有出现重复订单编号
Thread-5-->生成订单编号:2019-08-18-19-48-36-1
Thread-13-->生成订单编号:2019-08-18-19-48-36-2
Thread-18-->生成订单编号:2019-08-18-19-48-36-3
Thread-1-->生成订单编号:2019-08-18-19-48-36-4
Thread-8-->生成订单编号:2019-08-18-19-48-36-5
Thread-10-->生成订单编号:2019-08-18-19-48-36-6
Thread-14-->生成订单编号:2019-08-18-19-48-36-7
Thread-19-->生成订单编号:2019-08-18-19-48-36-8
Thread-9-->生成订单编号:2019-08-18-19-48-36-9
Thread-6-->生成订单编号:2019-08-18-19-48-36-10
Thread-16-->生成订单编号:2019-08-18-19-48-36-11
Thread-3-->生成订单编号:2019-08-18-19-48-36-12
Thread-2-->生成订单编号:2019-08-18-19-48-36-13
Thread-12-->生成订单编号:2019-08-18-19-48-36-14
Thread-0-->生成订单编号:2019-08-18-19-48-36-15
Thread-11-->生成订单编号:2019-08-18-19-48-36-16
Thread-4-->生成订单编号:2019-08-18-19-48-36-17
Thread-7-->生成订单编号:2019-08-18-19-48-36-18
Thread-17-->生成订单编号:2019-08-18-19-48-36-19
Thread-15-->生成订单编号:2019-08-18-19-48-36-20
当然,伴随着Zookeeper的流行,目前已经有基于zk的分布式锁实现框架了,Curator,它是Netflix开源的一套ZooKeeper客户端框架,它提供了zk场景的绝大部分实现,本文就不介绍Curator分布式锁的实现方案了,关键是要理解上面原理思路。
看完了本文,你掌握了zk分布式锁了吗 0.0
查看原文,请关注下如下微信公众号“阿甘正专”,定期有软文分享哦
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/128909.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...