要实现分布式锁,最简单的?方式可能就是直接创建?一张锁表,然后通过操作该表中的数据来实现了了。
当我们要锁住某个?法或资源时,我们就在该表中增加一条记录,想要释放锁的时候就删除这条记录。
比如创建这样一张数据库表:
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:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因为我们对method_name做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执方法体内容。
当?法执行完毕之后,想要释放锁的话,需要执?行行以下sql:
delete from methodLock where method_name =‘method_name‘
上面说到这种方式基本废弃,那么这种简单的实现会存在哪些问题呢?
public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from methodLock where method_name=xxx
for update;
if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
在查询语句后?增加for update,数据库会在查询过程中给数据库表增加排他锁。当某条记录被加上排他锁之后,其他线程将无法再在该行行记录上增加排他锁。
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执?方法的业务逻辑,执行完之后,通过connection.commit()操作来释放锁。 这种方法可以有效的解决上?提到的?法释放锁和阻塞锁的问题。
阻塞锁? for update语句会在执行成功后?即返回,在执行失败时?直处于阻塞状态,直到成功。锁定之后 服务宕机,?法释放?使?这种?式,服务宕机之后数据库会自己把锁释放掉。但是还是?法直接解决数据库单点和可重?问题。
public void unlock(){
connection.commit();
}
说了这么多,我们总结下数据库方式实现。
?
总结 这两种方式都是依赖数据库的一张表,一种是通过表中的记录的存在情况确定当前是否有锁存在,另外一种是通过数据库的排他锁来实现分布式锁。
优点: 直接借助数据库,容易理解。
缺点: 会有各种各样的问题,在解决问题的过程中会使整个?案变得越来越复杂。 操作数据库需要一定的开销,性能问题也需要考虑。
?
redis实现分布式锁在电商开发中是使用的较为成熟和普遍的一种方式,利用redis本身特性及锁特性。如高性能(加、解锁时高性能),可以使用阻塞锁与非阻塞锁。不能出现死锁。通过搭建redis集群高可用性(不能出现节点 down 掉后加锁失败)。
尝试写伪代码增加理解,我们先看这种方式的分布式锁如何抢占。
/**
* @param key 锁的key
* @param lockValue 锁的value
* @param timeout 允许获取锁的时间,超过该时间就返回false
* @param expire key的缓存时间,也即一个线程?次持有锁的时间,
* @param sleepTime 获取锁的线程循环尝试获取锁的间隔时间
* @return
*/
public boolean tryLock(String key, String lockValue, Integer timeout, Integer
expire, Integer sleepTime) {
int st = (sleepTime == null) ? DEFAULT_TIME : sleepTime; //允许获取锁的时间,默认30秒
int expiredNx = 30;
final long start = System.currentTimeMillis();
if (timeout > expiredNx) {
timeout = expiredNx;
}
final long end = start + timeout * 1000; // 默认返回失败
boolean res ;
//如果尝试获取锁的时间超过了了允许时间,则直接返回
while (!(res = this.lock(key, lockValue, expire))) {
if (System.currentTimeMillis() > end) {
break;
}
try {
// 线程sleep,避免过度请求Redis,该值可以调整 Thread.sleep(st);
} catch (InterruptedException e) {
?
}
}
return res;
}
上?的讨论中我们有一个?常重要的假设:Redis是单点的。如果Redis是集群模式,我们考虑如下场景:
客户端1和客户端2同时持有了同一个资源的锁,锁不再具有安全性。根本原因是Redis集群不是强?致性的。
那么怎么保证强?致性呢—Redlock算法
假设客户端1从Master获取了锁。 这时候Master宕机了,存储锁的key还没有来得及同步到Slave上。 Slave升级为Master。 客户端2从新的Master获取到了对应同一个资源的锁。
redLock实现步骤:
优点:性能好
缺点:?法保证强?致性 (即能接受部分数据丢失)
原理
多个进程内同一时间都有线程在执行方法m,那么锁就一把,你获得了锁得以执行,我就得被阻塞,那你执行完了怎么来唤醒我呢?因为你并不知道我被阻塞了,你也就不能通知我" 嗨,小橘,我用完了,你用吧 "。你能做的只有用的时候设置锁标志,用完了再取消你设置的标志。我就必须在阻塞的时候隔一段时间主动去看看,但这样总归是有点麻烦的,最好有人来通知我可以执行了。
而zookeeper对于自身节点的两大特性解决了这个问题
zk中节点有类型区分吗?
有。zk中提供了四种类型的节点,各种类型节点及其区别如下:
?
持久节点(PERSISTENT):节点创建后,就一直存在,直到有删除操作来主动清除这个节点
持久顺序节点(PERSISTENT_SEQUENTIAL):保留持久节点的特性,额外的特性是,每个节点会为其第一层子节点维护一个顺序,记录每个子节点创建的先后顺序,ZK会自动为给定节点名加上一个数字后缀(自增的),作为新的节点名。
临时节点(EPHEMERAL):和持久节点不同的是,临时节点的生命周期和客户端会话绑定,当然也可以主动删除。
临时顺序节点(EPHEMERAL_SEQUENTIAL):保留临时节点的特性,额外的特性如持久顺序节点的额外特性。
?
如何操作节点?
节点的增删改查分别是create\delete\setData\getData,exists判断节点是否存在,getChildren获取所有子节点的引用。
?
上面提到了节点的监听者,我们可以在对zk的节点进行查询操作时,设置当前线程是否监听所查询的节点。getData、getChildren、exists都属于对节点的查询操作,这些方法都有一个boolean类型的watch参数,用来设置是否监听该节点。一旦某个线程监听了某个节点,那么这个节点发生的creat(在该节点下新建子节点)、setData、delete(删除节点本身或是删除其某个子节点)都会触发zk去通知监听该节点的线程。但需要注意的是,线程对节点设置的监听是一次性的,也就是说zk通知监听线程后需要改线程再次设置监听节点,否则该节点再次的修改zk不会再次通知。
实现
/**
* 尝试加锁
* @return
*/
public boolean tryLock() {
// 创建临时顺序节点
if (this.currentPath == null) {
// 在lockPath节点下面创建临时顺序节点
currentPath = this.client.createEphemeralSequential(LockPath + "/", "orangecsong");
}
// 获得所有的子节点
List<String> children = this.client.getChildren(LockPath);
?
// 排序list
Collections.sort(children);
?
// 判断当前节点是否是最小的,如果是最小的节点,则表明此这个client可以获取锁
if (currentPath.equals(LockPath + "/" + children.get(0))) {
return true;
} else {
// 如果不是当前最小的sequence,取到前一个临时节点
// 1.单独获取临时节点的顺序号
// 2.查找这个顺序号在children中的下标
// 3.存储前一个节点的完整路径
int curIndex = children.indexOf(currentPath.substring(LockPath.length() + 1));
beforePath = LockPath + "/" + children.get(curIndex - 1);
}
return false;
}
?
/**
* 等待锁
*/
private void waitForLock() {
// cdl对象主要是让线程等待
CountDownLatch cdl = new CountDownLatch(1);
// 注册watcher监听器
IZkDataListener listener = new IZkDataListener() {
@Override
public void handleDataDeleted(String dataPath) throws Exception {
System.out.println("监听到前一个节点被删除了");
cdl.countDown();
}
?
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
};
?
// 监听前一个临时节点
client.subscribeDataChanges(this.beforePath, listener);
?
// 前一个节点还存在,则阻塞自己
if (this.client.exists(this.beforePath)) {
try {
// 直至前一个节点释放锁,才会继续往下执行
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
?
// 醒来后,表明前一个临时节点已经被删除,此时客户端可以获取锁 && 取消watcher监听
client.unsubscribeDataChanges(this.beforePath, listener);
}
优点:?可用性,数据强一致性。多进程共享、可以存储锁信息、有主动通知的机制。
缺点:没有原??持锁操作,需借助 client 端实现锁操作,即加?次锁可能会有多次的网络请求;临时节点,若在网络抖动的情况即会导致锁对应的节点被?即释放,有一定概率会产?并发的情况。
原文:https://www.cnblogs.com/csong7876/p/13287955.html