[TOC]html
不少时候咱们须要保证同一时间一个方法只能被同一个线程调用,在单机环境中,Java中其实提供了不少并发处理相关的API,可是这些API在分布式场景中就无能为力了。也就是说单纯的Java Api并不能提供分布式锁的能力。java
针对分布式锁的实现目前有多种方案:redis
直接建一张表,里面记录锁定的方法名
时间
便可。
须要加锁时,就插入一条数据,释放锁时就删除数据。算法
CREATE TABLE `methodLock` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名',
`desc` varchar(1024) NOT NULL DEFAULT '备注信息',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';复制代码
当咱们想要锁住某个方法时,执行如下SQL:sql
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)复制代码
由于咱们对method_name作了惟一性约束,这里若是有多个请求同时提交到数据库的话,数据库会保证只有一个操做能够成功,那么咱们就能够认为
操做成功的那个线程得到了该方法的锁,能够执行方法体内容。
当方法执行完毕以后,想要释放锁的话,须要执行如下Sql:数据库
delete from methodLock where method_name ='method_name'复制代码
数据库实现分布式锁的优势: 直接借助数据库,容易理解。apache
数据库实现分布式锁的缺点: 会有各类各样的问题,在解决问题的过程当中会使整个方案变得愈来愈复杂。缓存
操做数据库须要必定的开销,性能问题须要考虑。安全
相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好一些。bash
目前有不少成熟的分布式产品,包括Redis、memcache、Tair等。
public Object around(ProceedingJoinPoint joinPoint) {
try {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
DLock dLock = method.getAnnotation(DLock.class);
if (dLock != null) {
String lockedPrefix = buildLockedPrefix(dLock, method, joinPoint.getArgs());
long timeOut = dLock.timeOut();
int expireTime = dLock.expireTime();
long value = System.currentTimeMillis();
if (lock(lockedPrefix, timeOut, expireTime, value)) {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
} finally {
unlock(lockedPrefix, value);
}
} else {
recheck(lockedPrefix, expireTime);
}
}
} catch (Exception e) {
logger.error("DLockAspect around error", e);
}
return null;
}
/** * 检查是否设置过超时 * * @param lockedPrefix * @param expireTime */
public void recheck(String lockedPrefix, int expireTime) {
try {
Result<Long> ttl = cacheFactory.getFactory().ttl(getLockedPrefix(lockedPrefix));
if (ttl.isSuccess() && ttl.getValue() == -1) {
Result<String> get = cacheFactory.getFactory().get(getLockedPrefix(lockedPrefix));
//没有超时设置则设置超时
if (get.isSuccess() && !StringUtils.isEmpty(get.getValue())) {
long oldTime = Long.parseLong(get.getValue());
long newTime = expireTime * 1000 - (System.currentTimeMillis() - oldTime);
if (newTime < 0) {
//已过超时时间 设默认最小超时时间
cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), MIX_EXPIRE_TIME);
} else {
//未超过 设置为剩余超时时间
cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), (int) newTime);
}
logger.info(lockedPrefix + "recheck:" + newTime);
}
}
logger.info(String.format("执行失败lockedPrefix:%s count:%d", lockedPrefix, count++));
} catch (Exception e) {
logger.error("DLockAspect recheck error", e);
}
}
public boolean lock(String lockedPrefix, long timeOut, int expireTime, long value) {
long millisTime = System.currentTimeMillis();
try {
//在timeOut的时间范围内不断轮询锁
while (System.currentTimeMillis() - millisTime < timeOut * 1000) {
//锁不存在的话,设置锁并设置锁过时时间,即加锁
Result<Long> result = cacheFactory.getFactory().setnx(getLockedPrefix(lockedPrefix), String.valueOf(value));
if (result.isSuccess() && result.getValue() == 1) {
Result<Long> result1 = cacheFactory.getFactory().expire(getLockedPrefix(lockedPrefix), expireTime);
logger.info(lockedPrefix + "locked and expire " + result1.getValue());
return true;
}
//短暂休眠,避免可能的活锁
Thread.sleep(100, RANDOM.nextInt(50000));
}
} catch (Exception e) {
logger.error("lock error " + getLockedPrefix(lockedPrefix), e);
}
return false;
}
public void unlock(String lockedPrefix, long value) {
try {
Result<String> result = cacheFactory.getFactory().get(getLockedPrefix(lockedPrefix));
String kvValue = result.getValue();
if (!StringUtils.isEmpty(kvValue) && kvValue.equals(String.valueOf(value))) {
cacheFactory.getFactory().del(getLockedPrefix(lockedPrefix));
}
logger.info(lockedPrefix + "unlock:" + kvValue + "----" + value);
} catch (Exception e) {
logger.error("unlock error" + getLockedPrefix(lockedPrefix), e);
}
}复制代码
Redlock是Redis的做者antirez给出的集群模式的Redis分布式锁,它基于N个彻底独立的Redis节点(一般状况下N能够设置成5)。
客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住);节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了;节点C重启后,客户端2锁住了C, D, E,获取锁成功。客户端1和客户端2同时得到了锁(针对同一资源)。
这个问题能够延迟节点的恢复时间,时间长度应大于等于一个锁的过时时间。
关于RedLock的更多内容能够看:
一个比较好的实现:
无单点问题。ZK是集群部署的,只要集群中有半数以上的机器存活,就能够对外提供服务。
持有锁任意长的时间,可自动释放锁。使用Zookeeper能够有效的解决锁没法释放的问题,由于在建立锁的时候,客户端会在ZK中建立一个临时节点,一旦客户端获取到锁以后忽然挂掉(Session链接断开),那么这个临时节点就会自动删除掉。其余客户端就能够再次得到锁。这避免了基于Redis的锁对于有效时间(lock validity time)到底设置多长的两难问题。实际上,基于ZooKeeper的锁是依靠Session(心跳)来维持锁的持有状态的,而Redis不支持Sesion。
可阻塞。使用Zookeeper能够实现阻塞的锁,客户端能够经过在ZK中建立顺序节点,而且在节点上绑定监听器,一旦节点有变化,Zookeeper会通知客户端,客户端能够检查本身建立的节点是否是当前全部节点中序号最小的,若是是,那么本身就获取到锁,即可以执行业务逻辑了。
可重入。客户端在建立节点的时候,把当前客户端的主机信息和线程信息直接写入到节点中,下次想要获取锁的时候和当前最小的节点中的数据比对一下就能够了。若是和本身的信息同样,那么本身直接获取到锁,若是不同就再建立一个临时的顺序节点,参与排队。
羊群效应
,从而下降锁的性能。一个比较好的实现:
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
try {
return interProcessMutex.acquire(timeout, unit);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
public boolean unlock() {
try {
interProcessMutex.release();
} catch (Throwable e) {
log.error(e.getMessage(), e);
} finally {
executorService.schedule(new Cleaner(client, path), delayTimeForClean, TimeUnit.MILLISECONDS);
}
return true;
}复制代码
acquire方法用户获取锁,release方法用于释放锁。
使用Zookeeper实现分布式锁的优势: 有效的解决单点问题,不可重入问题,非阻塞问题以及锁没法释放的问题。实现起来较为简单。
使用Zookeeper实现分布式锁的缺点 : 性能上不如使用缓存实现分布式锁。 须要对ZK的原理有所了解。
从理解的难易程度角度(从低到高): 数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高): Zookeeper >= 缓存 > 数据库
从性能角度(从高到低): 缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低): Zookeeper > 缓存 > 数据库