一、什么是分布式锁,它为什么会产生
现在我们的项目大多都是分布式部署的,分布式部署一个显而易见的好处就是可以减轻服务器的压力,但也带来一系列的问题,比如:分布式session一致性问题、分布式配置中心、分布式任务调度平台、分布式日志平台、分布式全局ID的生成、分布式事务、分布式限流、分布式锁。
由于项目是分布式部署的,这样部署的每一份都会有自己的jvm运行项目,但是它们需要共享一些实例变量,但是在高并发的情况下,可能有多个jvm同时操作实例变量,这样就会造成数据不一致、值不同步的问题,这个时候我们需要保证只有一个jvm在一个时间操作实例变量,这样我们就引用了分布式锁来限制只有一个jvm能执行任务,其他的先排队。
synchronized和lock只能解决单个jvm中多个线程的线程安全问题
二、分布式锁的常见解决方案,它们的优缺点是
(1) 使用数据库,我们知道数据库本身有表锁、行锁,但是使用数据库作分布式锁的效率非常低下
(2)使用redis,使用redis本身自带的setNx命令,这是一个互斥的命令,如果setNx需要创建的key在redis中已经有了,那么其他请求不能成功返回异常。 好处:我们知道redis的写的效率是81000,读的效率是110000,因此它的效率还是可以的。 缺点:创建分布式锁容易造成死锁,不好控制过期时间,而且有一个bug就是执行任务删除锁,需要先判断一下这个锁是否存在,返回正确的情况下,这个时候恰好过期了,而新锁又由其他线程创建,这个时候去删除就会删除掉别的线程的锁,需要使用redis+lua脚本解决这个问题,一旦发现自己的锁,立即删除。
(3)RedisSon 是专门用来做分布式锁的
(4)zookeeper做分布式锁,比较简单,不同意造成死锁
三、zookeeper实现分布式锁的原理
使用zookeeper的创建的是临时节点的原理,为什么要创建临时节点是因为zookeeper的连接session close以后。临时节点自动消失。
(1)多个jvm中的项目中的线程去zookeeper中创建同一个节点,这个节点是临时节点
(2)创建临时节点的结果可能是一个成功,其他失败,这样成功创建节点的相当于拿到了锁,可以执行任务,没有创建成功相当于没有拿到锁,那么进行等待
(3)拿到锁的执行完任务之后,那么就可以释放锁(close掉zookeeper的session,强制关闭会造成短暂死锁),通过watcher通知给其他的jvm,重新去创建节点,竞争锁。
四、代码演示
zk实现分布式锁
/**
* Created by 辉 on 2020/6/30.
*/
public class ZookeeperLock {
// zk连接地址
private static final String CONNECTSTRING = "192.168.196.175:2181";
// 创建zk连接
protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
//防止死锁的发生,在创建zk连接的时候可以传入session的失效
//时间,这样就能在zk断开的时候自动去除这个临时节点
protected static final String PATH = "/lock";
public void getLock() {
if (tryLock()) {
System.out.println("##获取lock锁的资源####");
} else {
// 等待
waitLock();
// 重新获取锁资源
getLock();
}
}
public void unLock() {
if (zkClient != null) {
zkClient.close();
System.out.println("释放锁资源...");
}
}
private CountDownLatch countDownLatch = null;
boolean tryLock() {
try {
zkClient.createEphemeral(PATH);
return true; //创建临时节点成功,返回true
} catch (Exception e) {
// e.printStackTrace();
return false; //创建节点失败,那么就会出异常这里就会捕获,这样就会到这里,返回false
}
}
void waitLock() {
IZkDataListener izkDataListener = new IZkDataListener() {
public void handleDataDeleted(String path) throws Exception {
// 唤醒被等待的线程
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
public void handleDataChange(String path, Object data) throws Exception {
}
};
// 注册事件
zkClient.subscribeDataChanges(PATH, izkDataListener);
if (zkClient.exists(PATH)) {
countDownLatch = new CountDownLatch(1);
try {
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
// 删除监听
zkClient.unsubscribeDataChanges(PATH, izkDataListener);
}
}
|
订单类
//生成订单类
public class OrderNumGenerator {
//全局订单id
public static int count = 0;
public String getNumber() {
try {
Thread.sleep(200);
} catch (Exception e) {
}
SimpleDateFormat simpt = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
return simpt.format(new Date()) + "-" + ++count;
}
}
|
生成全局订单ID
//使用多线程模拟生成订单号
public class OrderService implements Runnable {
private OrderNumGenerator orderNumGenerator = new OrderNumGenerator();
ZookeeperLock zkLock=new ZookeeperLock();
public void run() {
getNumber();
}
public void getNumber() {
try {
zkLock.getLock();
String number = orderNumGenerator.getNumber();
System.out.println(Thread.currentThread().getName() + ",生成订单ID:" + number);
} catch (Exception e) {
e.printStackTrace();
} finally {
zkLock.unLock();
}
}
public static void main(String[] args) {
System.out.println("####生成唯一订单号###");
for (int i = 0; i < 100; i++) {
new Thread(new OrderService()).start();
}
}
|
效果: