常见问题:
使用zk实现分布式锁,,如果主节点宕机:会发生选举的过程,为了数据一致性的问题,在zk选举的过程中整个zk环境无法使用,客户端获取锁的时候,会不断的重试,重试多次会直接抛出异常。
客户端已经在监听的情况下:
zk客户端事件监听到当前的leader节点已经宕机,会自动实现重试。
该过程可能在剩余的节点中选出一个新的leader节点,从新监听新的leade节点,,如果一直没有选出leader节点,为了避免客户端死锁的问题,从新获取锁的状态。
使用zk实现分布式锁,死锁的问题:
curator概念:
Curator是Netflix公司开源的一套zookeeper客户端框架,解决了很多Zookeeper客户端非常底层的细节开发工作,包括连接重连、反复注册Watcher和NodeExistsException异常等等。Patrixck Hunt(Zookeeper)以一句“Guava is to Java that Curator to Zookeeper”给Curator予高度评价。
Curator官网
其中Curator提供分布式锁实现方案,解决羊群效应问题。
核心API:
InterProcessMutex:分布式可重入排它锁
InterProcessSemaphoreMutex:分布式排它锁
InterProcessReadWriteLock:分布式读写锁
InterProcessMultiLock:将多个锁作为单个实体管理的容器
Curator环境搭建
Maven依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
核心api配置
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.utils.CloseableUtils;
import org.springframework.stereotype.Component;
/**
* @ClassName CuratorZkLock zk框架实现分布式锁
* @Author 蚂蚁课堂余胜军 QQ644064779 www.mayikt.com
* @Version V1.0
**/
@Component
public class CuratorZkLock {
// ZooKeeper 服务地址, 单机格式为:(127.0.0.1:2181),
// 集群格式为:(127.0.0.1:2181)
private String connectString;
// Curator 客户端重试策略
private RetryPolicy retry;
// Curator 客户端对象
private CuratorFramework client;
public CuratorZkLock() throws Exception {
init();
}
public void init() throws Exception {
// 设置 ZooKeeper 服务地址为本机的 2181 端口
connectString = "127.0.0.1:2181";
// 重试策略
// 初始休眠时间为 1000ms, 最大重试次数为 3
retry = new ExponentialBackoffRetry(1000, 3);
// 创建一个客户端, 60000(ms)为 session 超时时间, 15000(ms)为连接超时时间
client = CuratorFrameworkFactory.newClient(connectString, 60000, 15000, retry);
client.start();
}
public void close() {
CloseableUtils.closeQuietly(client);
}
public CuratorFramework getClient() {
return client;
}
}
简单用法
/**
秒杀lock
-
@return
*/
@RequestMapping("/seckilLock")
public String seckilLock() throws Exception {
InterProcessMutex interProcessMutex = null;
try {
CuratorFramework client = curatorZkLock.getClient();
interProcessMutex = new InterProcessMutex(client, lockPath);
interProcessMutex.acquire();
log.info("<获取锁成功....>");
log.info("<执行秒杀扣库存业务逻辑....>");
CommodityDetails commodityDetails = commodityDetailsMapper.getCommodityDetails(1L);
Long stock = commodityDetails.getStock();
if (stock > 0) {
log.info("<扣库存成功>");
commodityDetailsMapper.reduceInventory(1l);
return "ok";
}
// 扣库存失败
log.info("<扣库存失败>");
return "fail";} catch (Exception e) {
e.printStackTrace();
log.info("<秒杀出现错误:e:{}>", e);
return "fail";
} finally {
if (interProcessMutex != null)
interProcessMutex.release();
}
}
Curator源码解读
- 从缓存中查找是否已经创建分布式锁,如果已经创建了分布式锁
则直接复用(具有可重入性) - 如果缓存中没有,则创建一个分布式锁
- 实现原理:创建一个临时节点,获取当前父节点下子节点,如果是为最小的节点,则表示获取锁成功,否则获取锁失败,阻塞等待,则监听上一个节点。
- 当上一个节点如果释放锁之后,直接进入到获取锁的状态。
唤醒使用wait notify技术
获取到当前的线程
Thread currentThread = Thread.currentThread();
从集合中查询当前线程是否有获取过分布式锁;
LockData lockData = threadData.get(currentThread);
if ( lockData != null )
{
// re-entering
如果有获取过分布式锁的情况下,则重入次数就会+1
lockData.lockCount.incrementAndGet();
return true;
}
在集合中没有查询到当前线程有获取过锁,如果没有获取锁的情况下,则创建一个临时顺序编号节点
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
if ( lockPath != null )
{
在将该临时顺序编号节点存放到集合缓存。
Key为当前线程 value 分布式锁
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
创建一个临时顺序编号节点
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
判断该临时顺序编号节点是否属于最小的;如果属于最小的节点
就表示获取锁成功;
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
获取当前节点对应 index下标位置;
int ourIndex = children.indexOf(sequenceNodeName);
validateOurIndex(sequenceNodeName, ourIndex);
0 maxLeases=1;
getsTheLock =true
getsTheLock =true 表示获取锁成功
boolean getsTheLock = ourIndex < maxLeases;
如果getsTheLock =false的情况下,则订阅我们上一个节点
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);