分布式改造剧集2---DIY分布式锁
前言:
好了,终于又开始播放分布式改造剧集了。前面一集中(http://www.cnblogs.com/Kidezyq/p/8748961.html)我们DIY了一个Hessian转发实现,最后我们也留下了一个展望方向:可以实现一个管理界面管理节点,实现简单的服务治理的功能。这一集我们接着继续DIY分布式锁。
第二集:分布式锁DIY
探索之路
由于业务互斥的需要,当前项目中实现了一个内存锁。锁的大致模型是分为锁类型和锁键值,只有当锁类型和键值都相同的时候,整个业务才互斥。但是必须提供一个方法,来判断某种类型的锁是否存在。大致代码如下:
/**
* 内存锁
*
*/
public class MemoryLock {
/**
* 同步锁
*/
private final Object lock = new Object();
/**
* 内存锁模型
*/
private ConcurrentHashMap<String, ConcurrentHashMap<String, String>> lockMap = new ConcurrentHashMap<String, ConcurrentHashMap<String, String>>();
/**
* 尝试获取到锁
* @param lockType 锁类型
* @param key 锁键值
* @return 如果当前获取到锁,则返回true。否则,返回false。
*/
private boolean tryLock(String lockType, String key) {
synchronized (this.lock) {
ConcurrentHashMap<String, String> map = this.lockMap.get(lockType);
if (map == null) {
map = new ConcurrentHashMap<String, String>();
this.lockMap.put(lockType, map);
}
return (map.putIfAbsent(key, key) == null);
}
}
/**
* 判断某种类型的锁是不是空的
* @param lockType 锁类型
* @return true,不存在某种类型的锁;false,存在某种类型的锁。
*/
public boolean isLockTypeEmpty(String lockType) {
if (null != this.lockMap.get(lockType)) {
return this.lockMap.get(lockType).size() == 0;
}
return true;
}
/**
* 获取锁
* @param lockType 锁类型
* @param key 锁键值
* @param timeout 超时时间(毫秒)
* @throws TimeoutException 如果超时之后还没有获得到锁,则抛出超时异常
*/
public void lock(String lockType, String key, long timeout) throws TimeoutException {
// 是否没有超时设置,当传入的超时时间为负数或者为0时,表示没有超时时间
boolean noTimeOutFlag = false;
if (timeout <= 0L) {
noTimeOutFlag = true;
}
long expireTime = System.currentTimeMillis() + timeout;
do {
if (tryLock(lockType, key))
return;
try {
Thread.sleep(100L);
} catch (InterruptedException localInterruptedException) {
}
} while ((noTimeOutFlag) || (System.currentTimeMillis() < expireTime));
throw new TimeoutException();
}
/**
* 释放锁
* @param lockType 锁类型
* @param key 锁键值
*/
public void unlock(String lockType, String key) {
synchronized (this.lock) {
ConcurrentMap<String, String> map = this.lockMap.get(lockType);
if (map != null)
map.remove(key);
}
}
}
可以看到,单机模式下的互斥锁是直接在内存中保存一个ConcurrentHashMap
,然后利用putIfAbsent
的原子特性。该锁的使用方式如下:
try {
memoryLock.lock(lockType, lockKey, 0l);
} catch(TimeOutException e) {
// TODO: Exception caught
} finally {
memoryLock.unlock(lockType, lockKey);
}
当应用部署在分布式环境中的时候。显然,原来的内存锁已经不适用。那么在分布式情况下,如何实现锁服务呢?网上给出的分布式锁的实现方案一般有三种:
- 利用数据库的for update行锁
- 利用Redis的setnx
- 利用zookeeper的分布式一致性算法
考虑到尽量不增加新的应用部署,那么先排除2、3,只剩下数据库的行级锁。但其实数据库的行级锁在并发量特别大的时候会对数据库性能造成较大影响,而且估计我想使用DBA都不会允许.....
那么,有没有什么其他更好的办法呢?这次我们利用曲线救国
的方式来实现,将分布式转变成非分布式。
实现Demo
在分布式改造剧集第一集中,我们的实现方式中有一个主节点,主节点为配置文件中默认配置的Hessian服务的地址。只有加上了Distribute
注解的服务,才会在客户端进行Hessian调用的时候进行路由,否则最终调用的Hessian服务地址即为配置文件中配置的主节点。依赖于这个特性,我们可以不给锁服务添加Distribute
注解,使得所有分机部署的服务请求都落到主节点上。具体实现步骤如下:
定义一个内存锁Hessian服务
其实简单来说我们直接将原来的MemoryLock
发布成Hessian服务,并且不使用Distribute
注解就可以实现将分布式锁转换成单机锁。但是还有以下两点需要特殊考虑:
- 分布式服务的多机特性: 内存锁的释放必须显示释放,如果一个服务调用
unlock
方法之前就挂掉,就可能导致某一个锁永远被锁住。所以我们还需要一个类似于Redis分布式锁实现中的锁超时移除机制。- 远程RPC调用的可能超时: 最终锁的服务调用是需要通过Hessian来实现的,考虑到Hessian调用存在超时时间,如果将前面
MemoryLock
的lock
方法等待实现在Hessian服务中,那么等待时间超长的话会直接导致Hessian服务调用超时。所以改造后的MemoryLock
不实现lock
方法,只实现tryLock
方法,调用该方法时立即返回当前是否可以获得到锁。- 本地服务实现锁等待以及减少Hessian调用: 如第2点所说,我们的锁等待特性不能在内存锁的Hessian服务中实现,只能通过本地服务中实现。另外频繁的Hessian调用会影响应用程序的性能,也需要一个本地的锁服务来巧妙地减少远程服务调用
改造后的MemoryLock
代码如下:
@Service("moemoryLockServiceFacade")
public class MemoryLockServiceImpl implements MemoryLockService {
/**
* 自动超时时间:当前设置为10分钟 单位为纳秒
*/
private final static long AUTO_EXPIRE_TIME = 1000000000l * 60 * 10;
/**
* 锁
*/
private Object semaphore = new Object();
/**
* 内存锁结构,双层Map 首层Map的Key存锁类型,value为内层Map。内层Map额key为锁键值,value为锁的加入时间
*/
private ConcurrentMap<String, ConcurrentMap<Object, Long>> lockMap = new ConcurrentHashMap<String, ConcurrentMap<Object, Long>>();
/**
* 守护线程: 用来清理过期内存缓存(如果加锁的客户端由于各种原因没有显示解锁,则可能出现其他服务无法获取锁的情况)
*/
private Thread daemonThread;
private static final Logger LOGGER = LoggerFactory.getLogger(MemoryLockServiceImpl.class);
/**
* 是否终止守护线程的标识
*/
private volatile boolean stop = false;
/**
* 清理失效锁的线程
*
*/
private class ClearExpireLockThread extends Thread {
@Override
public void run() {
Iterator<Entry<String, ConcurrentMap<Object, Long>>> outerIterator = null;
Iterator<Entry<Object, Long>> innerIterator = null;
// 清理超过超时时间的锁
while (!stop) {
synchronized (semaphore) {
long expireNanoTimes = System.nanoTime() - AUTO_EXPIRE_TIME; // 算出超时时间,小于该时间的缓存都应该被移除
outerIterator = lockMap.entrySet().iterator();
while (outerIterator.hasNext()) {
Entry<String, ConcurrentMap<Object, Long>> entrySet = outerIterator.next();
innerIterator = entrySet.getValue().entrySet().iterator();
boolean allDeleted = true; // 是否全部删除的标识,默认设为true
while (innerIterator.hasNext()) {
Entry<Object, Long> innerEntry = innerIterator.next();
if (expireNanoTimes > innerEntry.getValue()) {
innerIterator.remove();
LOGGER.info("守护线程移除类型为【{}】键值为【{}】的锁......", entrySet.getKey(), innerEntry.getKey());
} else {
allDeleted = false;
}
}
// 如果所类型下的所有锁都被清除,则锁类型也该被移除
if (allDeleted) {
outerIterator.remove();
LOGGER.info("守护线程移除类型为【{}】的锁......", entrySet.getKey());
}
}
}
try {
// 如果超时时间为1秒,则等待千分之一秒
Thread.sleep(AUTO_EXPIRE_TIME / 1000000000l);
} catch (InterruptedException e) {
}
}
}
}
/**
* 终止守护线程
*/
@PreDestroy
public void stopDeamonThread() {
this.stop = true;
this.daemonThread.interrupt();
}
/**
* 初始化守护线程,用来扫描移除超时的内存锁
*/
@PostConstruct
public void initDeamonThread() {
daemonThread = new ClearExpireLockThread();
daemonThread.setDaemon(true);
daemonThread.start();
}
@Override
public boolean tryLock(String lockType, Object key) {
synchronized (this.semaphore) {
ConcurrentMap<Object, Long> map = (ConcurrentMap<Object, Long>) this.lockMap.get(lockType);
if (map == null) {
map = new ConcurrentHashMap<Object, Long>();
this.lockMap.put(lockType, map);
}
// 这里的value值设置为加锁的初始时间
return (map.putIfAbsent(key, System.nanoTime()) == null);
}
}
@Override
public boolean isLockTypeEmpty(String lockType){
return MapUtils.isEmpty(this.lockMap.get(lockType));
}
@Override
public void unlock(String lockType, Object key) {
synchronized (this.semaphore) {
ConcurrentMap<Object, Long> map = (ConcurrentMap<Object, Long>) this.lockMap.get(lockType);
if (map != null) {
map.remove(key);
LOGGER.info("手工释放类型为【{}】键值为【{}】的锁......", lockType, key);
}
}
}
}
定义一个分布式锁管理服务实现
定义一个DistributeLock
服务,该服务作为本地服务,用来实现锁等待以及减少Hessian锁请求调用。在本地锁服务中注入原来的内存锁Hessian服务实现。具体代码如下:
/**
* 分布式锁管理类
*
*/
@Service
public class DistributeLock {
/**
* 注入hessian接口的实现类
*/
@Resource(name="moemoryLockServiceFacade")
private MemoryLockService memoryLockService;
private Object semaphore = new Object();
/**
* 内存锁结构,双层Map 首层Map的Key存锁类型,value为内层Map。内层Map额key为锁键值,value为锁住的尝试远程hessian调用获取锁的线程
*/
private ConcurrentMap<String, ConcurrentMap<String, Thread>> lockMap = new ConcurrentHashMap<String, ConcurrentMap<String, Thread>>();
/**
* 判断是否能够获得锁,不阻塞立即返回
* @param lockType 锁类型
* @param key 锁的键值
* @return true,能够获得锁.false,不能获得锁.
*/
private boolean tryLock(String lockType, String key) {
// 提升效率,先内部map判断是否存在锁,如果存在,则直接等待
synchronized (this.semaphore) {
ConcurrentMap<String, Thread> map = (ConcurrentMap<String, Thread>) this.lockMap.get(lockType);
if (map == null) {
map = new ConcurrentHashMap<String, Thread>();
this.lockMap.put(lockType, map);
}
Thread t = map.putIfAbsent(key, Thread.currentThread());
// 单个服务只有首先获得本机内存锁的线程才有机会去远程调用hessian服务判断是否有锁
if (t != null && Thread.currentThread() != t) {
return false;
}
}
return memoryLockService.tryLock(lockType, key);
}
/**
* 获得锁,在获得锁之前阻塞
* @param lockType 锁类型
* @param key 锁键值
* @param timeout 超时时间
* @throws TimeoutException 超时抛出超时异常
*/
public void lock(String lockType, String key, long timeout) throws TimeoutException {
// 是否没有超时设置,当传入的超时时间为负数或者为0时,表示没有超时时间
boolean noTimeOutFlag = false;
if (timeout <= 0L) {
noTimeOutFlag = true;
}
long expireTime = System.currentTimeMillis() + timeout;
do {
if (tryLock(lockType, key))
return;
try {
Thread.sleep(100L);
} catch (InterruptedException localInterruptedException) {
}
} while ((noTimeOutFlag) || (System.currentTimeMillis() < expireTime));
synchronized(this.semaphore) {
// 需释放当前线程占用的本地内存锁
this.lockMap.get(lockType).remove(key, Thread.currentThread());
}
throw new TimeoutException();
}
/**
* 是否指定的锁类型,当前锁的数量为空
* @param lockType 锁类型
* @return true,当前锁类型的锁的数量为空;false,当前锁类型的锁锁的数量不为空
*/
public boolean isLockTypeEmpty(String lockType){
// 直接内部判断
if (MapUtils.isNotEmpty(lockMap.get(lockType))) {
return false;
}
// 内部判断成功还需远程调用判断
return memoryLockService.isLockTypeEmpty(lockType);
}
/**
* 释放锁
* @param lockType 锁类型
* @param key 锁的键值
*/
public void unlock(String lockType, String key) {
// 移除本机内存锁模型
synchronized (this.semaphore) {
ConcurrentMap<String, Thread> map = (ConcurrentMap<String, Thread>) this.lockMap.get(lockType);
if (map != null)
map.remove(key);
}
// 远程调用释放锁
memoryLockService.unlock(lockType, key);
}
}
好了,分布式锁的Demo顺利完成。使用的时候只要将原来的MemoryLock
替换成DistributeLock
即可。
展望
分布式锁的实现就到这里,其实现的本质在于将分布式转变成非分布式。这里也可以说我是钻了"分布式"的空子
分布式改造剧集2---DIY分布式锁的更多相关文章
- 分布式改造剧集之Redis缓存采坑记
Redis缓存采坑记 前言 这个其实应该属于分布式改造剧集中的一集(第一集见前面博客:http://www.cnblogs.com/Kidezyq/p/8748961.html),本来按照顺序 ...
- 分布式改造剧集三:Ehcache分布式改造
第三集:分布式Ehcache缓存改造 前言 好久没有写博客了,大有半途而废的趋势.忙不是借口,这个好习惯还是要继续坚持.前面我承诺的第一期的DIY分布式,是时候上终篇了---DIY分布式缓存. 探 ...
- ubuntu12.04+Elasticsearch2.3.3伪分布式配置,集群状态分片调整
目录 [TOC] 1.什么是Elashticsearch 1.1 Elashticsearch介绍 Elasticsearch是一个基于Apache Lucene(TM)的开源搜索引擎.能够快速搜索数 ...
- 大数据系列(3)——Hadoop集群完全分布式坏境搭建
前言 上一篇我们讲解了Hadoop单节点的安装,并且已经通过VMware安装了一台CentOS 6.8的Linux系统,咱们本篇的目标就是要配置一个真正的完全分布式的Hadoop集群,闲言少叙,进入本 ...
- 集中式vs分布式区别
记录一下我了解到的版本控制系统,集中式与分布式,它们之间的区别做下个人总结. 什么是集中式? 集中式开发:是将项目集中存放在中央服务器中,在工作的时候,大家只在自己电脑上操作,从同一个地方下载最新版本 ...
- iOS开发——源代码管理——git(分布式版本控制和集中式版本控制对比,git和SVN对比,git常用指令,搭建GitHub远程仓库,搭建oschina远程仓库 )
一.git简介 什么是git? git是一款开源的分布式版本控制工具 在世界上所有的分布式版本控制工具中,git是最快.最简单.最流行的 git的起源 作者是Linux之父:Linus Bened ...
- EhCache 分布式缓存/缓存集群
开发环境: System:Windows JavaEE Server:tomcat5.0.2.8.tomcat6 JavaSDK: jdk6+ IDE:eclipse.MyEclipse 6.6 开发 ...
- EhCache 分布式缓存/缓存集群(转)
开发环境: System:Windows JavaEE Server:tomcat5.0.2.8.tomcat6 JavaSDK: jdk6+ IDE:eclipse.MyEclipse 6.6 开发 ...
- 集中式vs分布式
Linus一直痛恨的CVS及SVN都是集中式的版本控制系统,而Git是分布式版本控制系统,集中式和分布式版本控制系统有什么区别呢? 先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候 ...
随机推荐
- 如何在node和vue前后端分离的项目中使用极客验证,用node的方式
1.用express的脚手架和vue-cli的脚手架搭建的项目目录如下图 2.在vue-client的src/component新建一个login.vue文件,作为登录页面,代码如下 <temp ...
- Entry的验证
Entry组件是支持验证输入的合法性的, 比如要求输入数字,你输入了字母就是非法. 实现该功能,需要通过设置validate,validatecommand,invalidcommand选项. 1.首 ...
- Text-文本撤销
#撤销操作 from tkinter import * master = Tk() #打开undo按钮 text=Text(master,width=30,height=5,undo=True) te ...
- 使用java客户端调用redis
Redis支持很多编程语言的客户端,有C.C#.C++.Clojure.Common Lisp.Erlang.Go.Lua.Objective-C.PHP.Ruby.Scala,甚至更时髦的Node. ...
- https原理通俗了解
摘要:本文尝试一步步还原HTTPS的设计过程,以理解为什么HTTPS最终会是这副模样.但是这并不代表HTTPS的真实设计过程.在阅读本文时,你可以尝试放下已有的对HTTPS的理解,这样更利于" ...
- getgpc($k, $t='GP'),怎么返回的是 NULL?
<?php /** * 实用小代码 * 获得GET POST COOKIS */ $html=<<<WORD <form method="post"& ...
- Leetcode 804. Unique Morse Code Words 莫尔斯电码重复问题
参考:https://blog.csdn.net/yuweiming70/article/details/79684433 题目描述: International Morse Code defines ...
- [LeetCode] Stickers to Spell Word 贴片拼单词
We are given N different types of stickers. Each sticker has a lowercase English word on it. You wou ...
- Python3玩转儿 机器学习(1)
机器学习的基础概念 数据 著名的鸢尾花数据 https://en.wikipedia.org/wiki/lris_flower_data_set lris setossa ...
- [HNOI 2011]卡农
Description 题库链接 在集合 \(S=\{1,2,...,n\}\) 中选出 \(m\) 个子集,满足三点性质: 所有选出的 \(m\) 个子集都不能为空. 所有选出的 \(m\) 个子集 ...