分布式改造剧集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是分布式版本控制系统,集中式和分布式版本控制系统有什么区别呢? 先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候 ...
随机推荐
- Eclipse中JavaSwing图形插件安装
1.在百度中搜索WindowBuilder,找到http://www.eclipse.org/windowbuilder/ 2.点击Download调转到页面: 因为我的eclipse版本是 3.点击 ...
- 框架学习之Struts2(四)---拦截器和标签
一.拦截器概述 1.1 在struts2框架中封装了很多功能,struts2里面封装的功能都是在拦截器里面,struts2里面又很多拦截器,但不是每次这些拦截器都执行,每次执行型默认的拦截器. 默认拦 ...
- scrapy.Spider的属性和方法
scrapy.Spider的属性和方法 属性: name:spider的名称,要求唯一 allowed_domains:允许的域名,限制爬虫的范围 start_urls:初始urls custom_s ...
- Collection集合框架详解
[Java的集合框架] 接口: collection map list set 实现类: ArryList HashSet HashMap LinkList LinkHash ...
- MySQL加载本地数据时出现1290(HY000)错误
- 实验吧_who are you?(盲注)
who are you? 翻翻源码,抓抓包,乱试一通都没有什么结果 题目中提示有ip,立马应该联想到X-Forwarded-For 虽然知道是这个方面的题,但完全不知道从何入手,悄咪咪去翻一下wp 才 ...
- 使用jquery.qrcode.js生成二维码
通常生成二维码的方式有两种:第一种是java代码的形式,第二种是通过Js方式. 在这里我做个记录,用js生成二维码,可以在官网下载源码:http://jeromeetienne.github.io/j ...
- C++中compile与build的区别
我在前面的博文就提到了GCC编译器工作的四个阶段:预处理.编译.汇编.链接. 感兴趣的同学可以参考:http://www.cnblogs.com/mlgjb/p/7708007.html compil ...
- C语言程序设计预备作业。
1. 阅读邹欣老师的博客--师生关系,针对文中的几种师生关系谈谈你的看法,你期望的师生关系是什么样的? 答:我理想中的师生关系是Coach/Trainee(健身教练/健身学员)的关系.因为邹老师就如同 ...
- Tensorflow从入门到精通之——Tensorflow基本操作
前边的章节介绍了什么是Tensorflow,本节将带大家真正走进Tensorflow的世界,学习Tensorflow一些基本的操作及使用方法.同时也欢迎大家关注我们的网站和系列教程:http://ww ...