Java分布式main函数启动,分布式锁实现的正确打开方式

1、分布式锁概述

1.一、分布式锁做用

1)在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行html

2)具有高可用、高性能的获取锁与释放锁java

3)具有锁失效机制,防止死锁node

4)具有非阻塞锁(没有获取到锁将直接返回获取锁失败)或堵塞锁特性(根据业务需求考虑)redis

1.二、分布式锁应用场景

1)库存扣减与增长服务器

分布式锁保证库存扣减不会超卖,库存增长不会形成库存数据不许确网络

2)积分抵现session

防止积分扣减出现溢出的状况并发

3)会员礼品核销异步

防止礼品核销屡次分布式

1.三、实现方式

1)使用Redis,基于setnx命令或其余。

2)使用ZooKeeper,基于临时有序节点。

3)使用MySQL,基于惟一索引

2、基于Zookeeper实现分布式锁

2.一、Zookeeper特性介绍

1)有序节点

假如当前有一个父节点为/lock,咱们能够在这个父节点下面建立子节点;zookeeper提供了一个可选的有序特性,例如咱们能够建立子节点“/lock/node-”而且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号,也就是说若是是第一个建立的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。

2)临时节点

客户端能够创建一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。

3)事件监听

在读取数据时,咱们能够同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有以下四种事件:节点建立、节点删除、节点数据修改、子节点变动。

2.二、Zookeeper分布式锁实现(方式一)

2.2.一、实现原理

1)客户端链接zookeeper,并在父节点(/lock)下建立临时的且有序的子节点,第一个客户端对应的子节点为/lock/lock-1,第二个为/lock/lock-2,以此类推。

2)客户端获取/lock下的子节点列表,判断本身建立的子节点是否为当前子节点列表中序号最小的子节点,若是是则认为得到锁,不然监听/lock的子节点变动消息,得到子节点变动通知后重复此步骤直至得到锁;

3)执行业务代码;

4)完成业务流程后,删除对应的子节点释放锁。

2.2.二、实现代码

1.基于curator的zookeeper分布式锁实现

public static void main(String[] args) throws Exception {

//建立zookeeper的客户端

RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);

CuratorFramework client = CuratorFrameworkFactory.newClient("10.21.41.181:2181,10.21.42.47:2181,10.21.49.252:2181", retryPolicy);

client.start();

//建立分布式锁, 锁空间的根节点路径为/curator/lock

InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock");

mutex.acquire();

//得到了锁, 进行业务流程

System.out.println("Enter mutex");

//完成业务流程, 释放锁

mutex.release();

//关闭客户端

client.close();

}

2.实现方式二

1)定义变量

/**

* Zookeeper客户端

*/

private ZooKeeper zookeeper;

/**

* 锁的惟一标识

*/

private String lockId;

/**

* 与Zookeeper创建会话的信号量

*/

private CountDownLatch connectedLatch;

/**

* 建立分布式锁的过程当中,开始和等待请求建立分布式锁的信号标志

*/

private CountDownLatch creatingLatch;

/**

* 分布式锁路径前缀

*/

private String locksRootPath = "/locks";

/**

* 排在当前节点前面一位的节点的路径

*/

private String waitNodeLockPath;

/**

* 为了得到锁,本次建立的节点的路径

*/

private String currentNodeLockPath;

2)构造函数

public ZookeeperTempOrderLock(String lockId) {

this.lockId = lockId;

try {

// 会话超时时间

int sessionTimeout = 30000;

//

zookeeper = new ZooKeeper("192.168.0.93:2181", sessionTimeout, this);

connectedLatch.await();

} catch (IOException ioe) {

log.error("与Zookeeper创建链接时出现异常", ioe);

} catch (InterruptedException ite) {

log.error("等待与Zookeeper会话创建完成时出现异常", ite);

}

}

3)实现Zookeeper的watcher

@Override

public void process(WatchedEvent event) {

if (Event.KeeperState.SyncConnected == event.getState()) {

connectedLatch.countDown();

}

if (creatingLatch != null) {

creatingLatch.countDown();

}

}

4)获取分布式锁

/**

* 获取锁

*/

public void acquireDistributedLock() {

try {

while(!tryLock()) {

// 等待前一项服务释放锁的等待时间 不能超过一次Zookeeper会话的时间

long waitForPreviousLockRelease = 30000;

waitForLock(waitNodeLockPath, waitForPreviousLockRelease);

}

} catch (InterruptedException | KeeperException e) {

log.error("等待上锁的过程当中出现异常", e);

}

}

public boolean tryLock() {

try {

// 建立顺序临时节点

currentNodeLockPath = zookeeper.create(locksRootPath + "/" + lockId,

"".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

// 查看刚刚建立的节点是否是最小节点

// 好比针对于这个同名节点,以前有其它服务曾申请建立过,所以Zookeeper中临时顺序节点形如:

// /locks/10000000000, /locks/10000000001, /locks/10000000002

List nodePaths = zookeeper.getChildren(locksRootPath, false);

Collections.sort(nodePaths);

if(currentNodeLockPath.equals(locksRootPath + "/" + nodePaths.get(0))) {

// 若是是最小节点,则表明获取到锁

return true;

}

// 若是不是最小节点,则找到比本身小1的节点 (紧挨着本身)

int previousLockNodeIndex = -1;

for (int i = 0; i < nodePaths.size(); i++) {

if(currentNodeLockPath.equals(locksRootPath + "/" + nodePaths.get(i))) {

previousLockNodeIndex = i-1;

break;

}

}

this.waitNodeLockPath = nodePaths.get(previousLockNodeIndex);

} catch (KeeperException | InterruptedException e) {

log.error("建立临时顺序节点失败", e);

}

return false;

}

6)等待其余服务释放锁

/**

* 等待其余服务释放锁

* 实际上就是在等待前一个临时节点被删除

*

* @param nodePath 但愿被删除的节点的相对路径

* @param waitTime 等待时长 单位:毫秒

*/

private boolean waitForLock(String nodePath, long waitTime) throws KeeperException, InterruptedException {

Stat stat = zookeeper.exists(locksRootPath + "/" + nodePath, true);

if (stat != null) {

this.creatingLatch = new CountDownLatch(1);

this.creatingLatch.await(waitTime, TimeUnit.MILLISECONDS);

this.creatingLatch = null;

}

return true;

}

7)释放分布式锁

/**

* 释放锁

* 实际上就是删除当前建立的临时节点

*/

public void releaseLock() {

log.info("准备删除的节点路径: " + currentNodeLockPath);

try {

zookeeper.delete(currentNodeLockPath, -1);

currentNodeLockPath = null;

zookeeper.close();

} catch (Exception e) {

log.error("删除节点失败", e);

}

}

2.三、Zookeeper分布式锁实现(方式二)

2.3.一、实现原理

假设有两个服务A、B但愿得到同一把锁,执行过程大体以下:

1)服务A向zookeeper申请得到锁,该请求将尝试在zookeeper内建立一个临时节点(ephemeral znode),若是没有同名的临时节点存在,则znode建立成功,标志着服务A成功的得到了锁。

2) 服务B向zookeeper申请得到锁,一样尝试在zookeeper内建立一个临时节点(名称必须与服务A的相同),因为同名znode已经存在,所以请求被拒绝。接着,服务B会在zk中注册一个监听器,用于监听临时节点被删除的事件。

3) 若服务A主动向zk发起请求释放锁,或者服务A宕机、断开与zk的网络链接,zk会将服务A(建立者)建立的临时节点删除。而删除事件也将马上被监听器捕获到,并反馈给服务B。最后,服务B再次向zookeeper申请得到锁。

2.3.二、实现代码

基于临时节点实现Zookeeper分布式锁

多个服务若是想竞争同一把锁,那就向Zookeeper发起建立临时节点的请求,若能成功建立则得到锁,不然借助监听器,当监听到锁被其它服务释放(临时节点被删除),则本身再请求建立临时节点,反复这几个步骤直到成功建立临时节点或者与zookeeper创建的会话超时。

步骤:

1)定义变量

/**

* 与Zookeeper成功创建链接的信号标志

*/

private CountDownLatch connectedSemaphore = new CountDownLatch(1);

/**

* 建立分布式锁的过程当中,开始和等待请求建立分布式锁的信号标志

*/

private CountDownLatch creatingSemaphore;

/**

* Zookeeper客户端

*/

private ZooKeeper zookeeper;

/**

* 分布式锁的过时时间 单位:毫秒

*/

private static final Long DISTRIBUTED_KEY_OVERDUE_TIME = 30000L;

2)构造函数

public ZookeeperLock() {

try {

this.zookeeper = new ZooKeeper("192.168.0.93:2181", 5000, new ZookeeperWatcher());

try {

connectedSemaphore.await();

} catch (InterruptedException ite) {

log.error("等待Zookeeper成功创建链接的过程当中,线程抛出异常", ite);

}

log.info("与Zookeeper成功创建链接");

} catch (Exception e) {

log.error("与Zookeeper创建链接时出现异常", e);

}

}

3)获取分布式锁

实际上就是在尝试建立临时节点znode

create(final String path, byte data[], List acl,CreateMode createMod)

path: 从根节点"/"到当前节点的全路径

data: 当前节点存储的数据 (因为这里只是借助临时节点的建立来实现分布式锁,所以无需存储数据)

acl: Access Control list 访问控制列表 主要涵盖权限模式(Scheme)、受权对象(ID)、授予的权限(Permission)这三个方面 OPEN_ACL_UNSAFE 彻底开放的访问控制 对当前节点进行操做时,无需考虑ACL权限控制

createMode: 节点建立的模式

EPHEMERAL(临时节点) 当建立节点的客户端与zk断开链接后,临时节点将被删除

EPHEMERAL_SEQUENTIAL(临时顺序节点)

PERSISTENT(持久节点)

PERSISTENT_SEQUENTIAL(持久顺序节点)

public boolean acquireDistributeLock(Long lockId) {

String path = "/product-lock-" + lockId;

try {

zookeeper.create(path, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

log.info("ThreadId=" + Thread.currentThread().getId() + "建立临时节点成功");

return true;

} catch (Exception e) {

// 若临时节点已存在,则会抛出异常: NodeExistsException

while (true) {

// 至关于给znode注册了一个监听器,查看监听器是否存在

try {

Stat stat = zookeeper.exists(path, true);

if (stat != null) {

this.creatingSemaphore = new CountDownLatch(1);

this.creatingSemaphore.await(DISTRIBUTED_KEY_OVERDUE_TIME, TimeUnit.MILLISECONDS);

this.creatingSemaphore = null;

}

zookeeper.create(path, "".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

return true;

} catch (Exception ex) {

log.error("ThreadId=" + Thread.currentThread().getId() + ",查看临时节点时出现异常", ex);

}

}

}

}

4)释放分布式锁

public void releaseDistributedLock(Long lockId) {

String path = "/product-lock-" + lockId;

try {

// 第二个参数version是数据版本 每次znode内数据发生变化,都会使version自增,但因为分布式锁建立的临时znode没有存数据,所以version=-1

zookeeper.delete(path, -1);

log.info("成功释放分布式锁, lockId=" + lockId + ", ThreadId=" + Thread.currentThread().getId());

} catch (Exception e) {

log.error("释放分布式锁失败,lockId=" + lockId, e);

}

}

5)创建Zookeeper的watcher

不管是zk客户端与服务器链接成功,仍是删除节点,watcher监听到的事件都是SyncConnected

private class ZookeeperWatcher implements Watcher {

@Override

public void process(WatchedEvent event) {

log.info("接收到事件: " + event.getState() + ", ThreadId=" + Thread.currentThread().getId());

if (Event.KeeperState.SyncConnected == event.getState()) {

connectedSemaphore.countDown();

}

if (creatingSemaphore != null) {

creatingSemaphore.countDown();

}

}

}

6)main方式运用

建立了两个线程,其中第一个线程先执行,且持有锁5秒钟才释放锁,第二个线程后执行,当且仅当第一个线程释放锁(删除临时节点)后,第二个线程才能成功获取锁。

public static void main(String[] args) throws InterruptedException{

long lockId = 20200730;

new Thread(() ->{

ZookeeperLock zookeeperLock = new ZookeeperLock();

System.out.println("ThreadId1=" + Thread.currentThread().getId());

System.out.println("ThreadId=" + Thread.currentThread().getId() + "获取到分布式锁: " + zookeeperLock.acquireDistributeLock(lockId));

try {

TimeUnit.SECONDS.sleep(5);

} catch (InterruptedException e) {

log.error("ThreadId=" + Thread.currentThread().getId() + "暂停时出现异常", e);

}

zookeeperLock.releaseDistributedLock(lockId);

}).start();

TimeUnit.SECONDS.sleep(1);

new Thread(() -> {

ZookeeperLock zookeeperLock = new ZookeeperLock();

System.out.println("ThreadId2=" + Thread.currentThread().getId());

System.out.println("ThreadId=" + Thread.currentThread().getId() + "获取到分布式锁: " + zookeeperLock.acquireDistributeLock(lockId));

}).start();

}

3、基于Redis实现分布式锁

3.一、普一般见实现方式

3.1.一、实现代码

public String deductStock() {

String lockKey = "product_001";

try {

/*Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "aaa"); //jedis.setnx

stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS); //设置超时*/

//为解决原子性问题将设置锁和设置超时时间合并

Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "aaa", 10, TimeUnit.SECONDS);

//未设置成功,当前key已经存在了,直接返回错误

if (!result) {

return "error_code";

}

//业务逻辑实现,扣减库存

....

} catch (Exception e) {

e.printStackTrace();

}finally {

stringRedisTemplate.delete(lockKey);

}

return "end";

}

3.2.二、问题分析

上述代码能够看到,当前锁的失效时间为10s,若是当前扣减库存的业务逻辑执行须要15s时,高并发时会出现问题:

线程1,首先执行到10s后,锁(product_001)失效

线程2,在第10s后一样进入当前方法,此时加上锁(product_001)

当执行到15s时,线程1删除线程2加的锁(product_001)

线程3,能够加锁 .... 如此循环,实际锁已经没有意义

3.2.三、解决方案

定义一个子线程,定时去查看是否存在主线程的持有当前锁,若是存在则为其延长过时时间。

3.二、基于Redission实现方式

3.2.一、Redission简介

Jedis是Redis的Java实现的客户端,其API提供了比较全面的Redis命令的支持。Redission也是Redis的客户端,相比于Jedis功能简单。Jedis简单使用阻塞的I/O和redis交互,Redission经过Netty支持非阻塞I/O。

Redission封装了锁的实现,其继承了java.util.concurrent.locks.Lock的接口,让咱们像操做咱们的本地Lock同样去操做Redission的Lock。

经常使用API:

RLock redissonLock = redission.getLock();

redissionLock.lock(30,TmieUnit.SECONDS);加锁并设置锁的存活时间

redissionLock.unLock();解锁

3.2.二、实现原理

6a71d7618286c0a0c9b416fcf31237f4.png

多个线程去执行lock操做,仅有一个线程可以加锁成功,其它线程循环阻塞。

加锁成功,锁超时时间默认30s,并开启后台线程(子线程),加锁的后台会每隔10秒去检测线程持有的锁是否存在,还在的话,就延迟锁超时时间,从新设置为30s,即锁延期。

对于原子性,Redis分布式锁底层借助Lua脚本实现锁的原子性。锁延期是经过在底层用Lua进行延时,延时检测时间是对超时时间timeout /3。

1)简单实现代码:

public String deductStockRedission() {

String lockKey = "product_001";

RLock rlock = redission.getLock(lockKey);

try {

rlock.lock();

//业务逻辑实现,扣减库存

....

} catch (Exception e) {

e.printStackTrace();

} finally {

rlock.unlock();

}

return "end";

}

2)分析Redission适用缘由:

1)redisson全部指令都经过lua脚本执行,redis支持lua脚本原子性执行

2)redisson设置一个key的默认过时时间为30s,若是某个客户端持有一个锁超过了30s怎么办?

redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁以后,每隔10秒帮你把key的超时时间设为30s

这样的话,就算一直持有锁也不会出现key过时了,其余线程获取到锁的问题了。保证了没有死锁发生

3)Redisson的可重入锁

Redis存储锁的数据类型是 Hash类型

Hash数据类型的key值包含了当前线程信息。

f399093952a47678c3f00936fb6b748f.png

3.2.三、问题分析及对应方案

1)主从同步问题

问题分析:

当主Redis加锁了,开始执行线程,若还未将锁经过异步同步的方式同步到从Redis节点,主节点就挂了,此时会把某一台从节点做为新的主节点,此时别的线程就能够加锁了,这样就出错了,怎么办?

解决方案:

​ 1)采用zookeeper代替Redis

因为zk集群的特色,其支持的是CP。而Redis集群支持的则是AP。

​ 2)采用RedLock

a71c12cb2d6369ddff4a47eb1c0105cc.png

假设有3个redis节点,这些节点之间既没有主从,也没有集群关系。客户端用相同的key和随机值在3个节点上请求锁,请求锁的超时时间应小于锁自动释放时间。当在2个(超过半数)redis上请求到锁的时候,才算是真正获取到了锁。若是没有获取到锁,则把部分已锁的redis释放掉。

public String deductStockRedlock() {

String lockKey = "product_001";

//TODO 这里须要本身实例化不一样redis实例的redission客户端链接,这里只是伪代码用一个redisson客户端简化了

RLock rLock1 = redisson.getLock(lockKey);

RLock rLock2 = redisson.getLock(lockKey);

RLock rLock3 = redisson.getLock(lockKey);

// 向3个redis实例尝试加锁

RedissonRedLock redLock = new RedissionRedLock(rLock1, rLock2, rLock3);

boolean isLock;

try {

// 500ms拿不到锁, 就认为获取锁失败。10000ms即10s是锁失效时间。

isLock = redLock.tryLock(500, 10000, TimeUnit.MILLISECONDS);

System.out.println("isLock = " + isLock);

if (isLock) {

//业务逻辑处理

...

}

} catch (Exception e) {

} finally {

// 不管如何, 最后都要解锁

redLock.unlock();

}

}

不太推荐使用。若是考虑高可用并发推荐使用Redisson,考虑一致性推荐使用zookeeper。

2)提升并发:分段锁

因为Redission实际上就是将并行的请求,转化为串行请求。这样就下降了并发的响应速度,为了解决这一问题,能够将锁进行分段处理:例如秒杀商品001,本来存在1000个商品,能够将其分为20段,为每段分配50个商品。

好比:

​ 将库存进行分段,放入redis中,例如1000库存,可分10段放入Redis

​ key的设计能够为Product:10001:0 | Product:10001:1 ....

​ Redis底层集群,将根据key,计算器槽位,放入不一样节点中

参考文章:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值