业务场景
在分布式情况,生成全局订单号ID
产生问题
在分布式(集群)环境下,每台JVM不能实现同步,在分布式场景下使用时间戳生成订单号可能会重复
分布式情况下,怎么解决订单号生成不重复
- 使用分布式锁
- 提前生成好,订单号,存放在redis取。获取订单号,直接从redis中取。
使用分布式锁生成订单号技术
1.使用数据库实现分布式锁
缺点:性能差、线程出现异常时,容易出现死锁
2.使用redis实现分布式锁
缺点:锁的失效时间难控制、容易产生死锁、非阻塞式、不可重入
3.使用zookeeper实现分布式锁
实现相对简单、可靠性强、使用临时节点,失效时间容易控制
什么是分布式锁
分布式锁一般用在分布式系统或者多个应用中,用来控制同一任务是否执行或者任务的执行顺序。在项目中,部署了多个tomcat应用,在执行定时任务时就会遇到同一任务可能执行多次的情况,我们可以借助分布式锁,保证在同一时间只有一个tomcat应用执行了定时任务
使用Zookeeper实现分布式锁
Zookeeper实现分布式锁原理
使用zookeeper创建临时序列节点来实现分布式锁,适用于顺序执行的程序,大体思路就是创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推……
封装工具类zkClient的实现
@Configuration
public class ZkSessionConfig {
private static final Logger logger = LoggerFactory.getLogger(ZkSessionConfig.class);
@Value("${zookeeper.address:localhost:2181}")
private static String COONECTSTRING="localhost:2181";
@Value("${zookeeper.timeout:5000}")
private static int sessionTimeout=5000;
private final static CountDownLatch countDownLatch = new CountDownLatch(1);
/**
* 配置读取的属性值跟havingValue做比较,如果一样则返回true;否则返回false
* 如果返回值为false,则该configuration不生效;为true则生效 matchIfMissing =
* true表示如果没有在application.properties设置该属性,则默认为条件符合
*
* @return
*/
@Bean(name = "zkClient")
@ConditionalOnProperty(name = "zk.enabled", havingValue = "true", matchIfMissing = false)
public ZooKeeper zkClient() {
ZooKeeper zooKeeper = null;
try {
zooKeeper = new ZooKeeper(COONECTSTRING, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 获取事件类型
EventType eventType = event.getType();
// zk 路径
String path = event.getPath();
// 如果当前的连接状态是成功的,则通过计算器去控制
if (KeeperState.SyncConnected == event.getState()) {
// 如果收到了服务端的响应事件,连接成功
if (EventType.None == eventType) {
// 如果建立建立成功,让后程序往下走
logger.info("【事件通知】,zk 建立连接成功!");
countDownLatch.countDown();
}else if (EventType.NodeCreated == eventType) {
logger.info("【事件通知】,新增node节点" + path);
} else if (EventType.NodeDataChanged == eventType) {
logger.info("【事件通知】,当前node节点" + path + "被修改....");
} else if (EventType.NodeDeleted == eventType) {
logger.info("【事件通知】,当前node节点" + path + "被删除....");
}
}
}
});
countDownLatch.await();
logger.info("【初始化ZooKeeper连接状态....】={}", zooKeeper.getState());
} catch (IOException | InterruptedException e) {
logger.error("【初始化ZooKeeper连接异常....】={}", e);
e.printStackTrace();
}
return zooKeeper;
}
public static void main(String[] args) {
try {
ZooKeeper zookeeper = new ZooKeeper(COONECTSTRING, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent event) {
// 获取事件类型
EventType eventType = event.getType();
// zk 路径
String path = event.getPath();
// 如果当前的连接状态是成功的,则通过计算器去控制
if (KeeperState.SyncConnected == event.getState()) {
// 如果收到了服务端的响应事件,连接成功
if (EventType.None == eventType) {
// 如果建立建立成功,让后程序往下走
logger.info("【事件通知】,zk 建立连接成功!");
countDownLatch.countDown();
}else if (EventType.NodeCreated == eventType) {
logger.info("【事件通知】,新增node节点" + path);
} else if (EventType.NodeDataChanged == eventType) {
logger.info("【事件通知】,当前node节点" + path + "被修改....");
} else if (EventType.NodeDeleted == eventType) {
logger.info("【事件通知】,当前node节点" + path + "被删除....");
}
}
}
});
String path="/edu_temp1";
countDownLatch.await();
System.out.println(zookeeper.getState());
zookeeper.exists(path, true);
// 1.创建持久节点,并且允许任何服务器可以操作
// String result1 = zookeeper.create("/edu_Lasting", "Lasting".getBytes(), Ids.OPEN_ACL_UNSAFE,
// CreateMode.PERSISTENT);
// System.out.println("result:" + result1);
// 2.创建临时节点
String result2 = zookeeper.create(path, "temp".getBytes(), Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
System.out.println("result:" + result2);
Thread.sleep(10000L);
} catch (IOException | InterruptedException | KeeperException e) {
e.printStackTrace();
}
}
}
@Component
@ConditionalOnProperty(name="zk.enabled", havingValue="true", matchIfMissing=false)
public class ZkSessionUtil {
private static final Logger logger = LoggerFactory.getLogger(ZkSessionUtil.class);
@Autowired
private ZooKeeper zkClient;
/**
* 判断指定节点是否存在
* @param path
* @param needWatch 指定是否复用zookeeper中默认的Watcher
* @return
*/
public Stat exists(String path, boolean needWatch){
try {
return zkClient.exists(path,needWatch);
} catch (Exception e) {
logger.error("【断指定节点是否存在异常】{},{}",path,e);
return null;
}
}
/**
* 检测结点是否存在 并设置监听事件
* 三种监听类型: 创建,删除,更新
*
* @param path
* @param watcher 传入指定的监听类
* @return
*/
public Stat exists(String path,Watcher watcher ){
try {
return zkClient.exists(path,watcher);
} catch (Exception e) {
logger.error("【断指定节点是否存在异常】{},{}",path,e);
return null;
}
}
/**
* 创建持久化节点
* @param path
* @param data
*/
public boolean createNode(String path, String data){
try {
zkClient.create(path,data.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
return true;
} catch (Exception e) {
logger.error("【创建持久化节点异常】{},{},{}",path,data,e);
return false;
}
}
/**
* 修改持久化节点
* @param path
* @param data
*/
public boolean updateNode(String path, String data){
try {
//zk的数据版本是从0开始计数的。如果客户端传入的是-1,则表示zk服务器需要基于最新的数据进行更新。如果对zk的数据节点的更新操作没有原子性要求则可以使用-1.
//version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查
zkClient.setData(path,data.getBytes(),-1);
return true;
} catch (Exception e) {
logger.error("【修改持久化节点异常】{},{},{}",path,data,e);
return false;
}
}
/**
* 删除持久化节点
* @param path
*/
public boolean deleteNode(String path){
try {
//version参数指定要更新的数据的版本, 如果version和真实的版本不同, 更新操作将失败. 指定version为-1则忽略版本检查
zkClient.delete(path,-1);
return true;
} catch (Exception e) {
logger.error("【删除持久化节点异常】{},{}",path,e);
return false;
}
}
/**
* 获取当前节点的子节点(不包含孙子节点)
* @param path 父节点path
*/
public List<String> getChildren(String path) throws KeeperException, InterruptedException{
List<String> list = zkClient.getChildren(path, false);
return list;
}
/**
* 获取指定节点的值
* @param path
* @return
*/
public String getData(String path,Watcher watcher){
try {
Stat stat=new Stat();
byte[] bytes=zkClient.getData(path,watcher,stat);
return new String(bytes);
}catch (Exception e){
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
}
}
创建Lock接口
public interface ZkClientLock {
// 成功 ---阻塞---失败
public void lock();
// 成功 --失败
public void unlock();
}
创建ZookeeperAbstractLock抽象类
public abstract class ZkClientAbstractLock implements ZkClientLock {
private static Logger logger = LoggerFactory.getLogger(ZkClientAbstractLock.class);
// // 定义session超时时间===>网络不稳定==》闪断
// 如果没有定义好SESSION_TIMEOUT,有可能在一个锁中会有两个进程在操作
// private static final int SESSION_TIMEOUT = 10000;
// 默认连接时间
// zk服务决定问题是否小号你的锁id对应的节点问题
// private static final int CONNETCTION_TIMEOUT = 1000;
//
protected static final String path = "/jetsen-zk-lock";
//
// // ZK服务字符串
// private String zkServers = "localhost:2181";
// public ZkClient zkClient = new ZkClient(zkServers, SESSION_TIMEOUT, CONNETCTION_TIMEOUT);
public ZkClient zkClient = new ZkClientConfig().getZkClient();
/**
* lock 是一个有阻塞中间状态的方法 1、高并发多线程上锁---》获取锁的行为====》如果失败,阻塞等待(中间的状态 )
* 2、获取锁成功====》进行业务方法的调用 3、等待的状态何时结束,结束的时候有没有通知唤醒===》再次尝试争抢锁
*/
@Override
public void lock() {
if (trylock()) {
// 成功===》执行业务代码
logger.info(Thread.currentThread().getName() + "get distrubtedLock success");
} else {
// 失败===》阻塞等待状态
waitForLock();
}
}
/**
*
*/
@Override
public void unlock() {
// // Spring 有效性检查
if (zkClient.exists(path)) {
zkClient.delete(path);
if (zkClient != null) {
zkClient.close();
}
}
}
// 扩展定义2中的中间状态
// 1、失败后等待(中间的状态)
protected abstract boolean waitForLock();
// 2 再次尝试争抢锁(中间的状态)
protected abstract boolean trylock();
}
public class ZkClientDistrbuteLockImpl extends ZkClientAbstractLock{
private static Logger logger = LoggerFactory.getLogger(ZkClientDistrbuteLockImpl.class);
private static CountDownLatch latch;
/**
* waitForLock 当期path被占用
* 设计等待方法
* 1、对当前分布式节点注册监听器,对删除事件感兴趣
* 如果节点被删除 ===》唤醒争抢====》waitForLock状态结束
*/
@Override
protected boolean waitForLock() {
//当前线程争抢锁失败的线程
//1、new一个监听器(ZK分布式架构使用)-===》放在分布式节点上面
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
logger.info("当前节点"+dataPath+"数据发生变化!");
}
//3、最关注的就是PATH节点的删除====》释放锁
@Override
public void handleDataDeleted(String dataPath) throws Exception {
//唤醒 限流器里面的线程,让一起去争抢
if(latch!=null){
latch.countDown();
//线程计数器-1 马上争抢
lock();
}
}
};
//2 创建出来的监听器listener注册到path
zkClient.subscribeDataChanges(path, listener);
//3 当前节点是否还存在
if(zkClient.exists(path)){
latch = new CountDownLatch(1);
}
zkClient.unsubscribeDataChanges(path, listener);
return false;
}
/**
* 通过在zk尝试获取分布式锁 一旦发现执行异常(ZkNodeException)说明你获取了锁失败
* 说明其中有一个线程获取锁成功,创建一个PATH节点
*/
@Override
protected boolean trylock() {
try {
zkClient.createEphemeral(path);
logger.info("创建分布式锁成功!");
return true;
} catch (Exception e) {
logger.info("创建分布式锁失败,已被其他线程占用");
return false;
}
}
}
public class ZkClientLockUtil {
public ZkClientLockUtil() {
}
public static String idGenerator( IGenerateGlobalId iGenerateGlobalId) {
ZkClientLock lock = new ZkClientDistrbuteLockImpl();
String idGenerator = null;
try {
lock.lock();
idGenerator = iGenerateGlobalId.idGenerator();
System.out.println(Thread.currentThread().getName() + ",生成ID:" + idGenerator);
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
return idGenerator;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
idGenerator(new SequenceUnLockGenerateGlobalIdImpl());
}
}
}
public class ZkClientTestTask implements Runnable {
private IGenerateGlobalId iGenerateGlobalId;
private CountDownLatch latch;
public ZkClientTestTask(IGenerateGlobalId iGenerateGlobalId, CountDownLatch latch) {
this.iGenerateGlobalId = iGenerateGlobalId;
this.latch = latch;
}
@Override
public void run() {
try {
String idGenerator = ZkClientLockUtil.idGenerator(iGenerateGlobalId);
System.out.printf("线程名称:%s 订单号:%s \r\n", Thread.currentThread().getName(),idGenerator);
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
}
}
public static void main(String[] args) {
IGenerateGlobalId entity = new SequenceUnLockGenerateGlobalIdImpl();
ExecutorService pool = Executors.newCachedThreadPool();
final CountDownLatch latch = new CountDownLatch(1);
for(int i=0;i<10;i++) {
pool.execute(new ZkClientTestTask(entity,latch));
}
latch.countDown();
pool.shutdown();
}
}