1、 redis实现分布式锁
1.1 setnx实现
setnx key value:当key不存在的时候,将key的值设置为value。若key存在则不做操作
怎么做
String lockKey="lockKey";
//set成功则为true,否则为false
Boolean result=redisTemplate.opsForValue().setIfAbsent(lockKey,"dbf");
if(!result){return error;}
//核心逻辑。。。
redisTemplate.delete(lockKey);
有什么问题
在释放锁之前异常了,会导致这个锁永远释放不了
怎么解决
放到try finally里,这样就能保证绝对能释放
try{
String lockKey="lockKey";
Boolean result=redisTemplate.opsForValue().setIfAbsent(lockKey,"dbf");
if(!result){return error;}
//核心逻辑。。。
}
finally{
redisTemplate.delete(lockKey);
}
有什么问题
直接挂了,finally都释放不了
怎么解决
那就给锁设置过期时间
try{
String lockKey="lockKey";
Boolean result=redisTemplate.opsForValue().setIfAbsent(lockKey,"dbf");
redisTemplate.expire(lockKey,10,TimeUnit.SECONDS);
if(!result){return error;}
//核心逻辑。。。
}
finally{
redisTemplate.delete(lockKey);
}
有什么问题
他在set之后,设置过期时间之前挂掉了呢
怎么解决
将两个操作合并成一个原子操作
try{
String lockKey="lockKey";
Boolean result=redisTemplate.opsForValue().setIfAbsent(lockKey,"dbf",10,TimeUnit.SECONDS);
if(!result){return error;}
//核心逻辑。。。
}
finally{
redisTemplate.delete(lockKey);
}
有什么问题
如果执行核心逻辑的时间超过了超时时间,那么他会删掉其他线程设置的锁,而且仍会导致并发问题
怎么解决
给线程加一个唯一id,删的时候对比一下id(通过lua脚本),防止删掉其他线程的锁。而且在运行时不断的(比如每隔5s)去重置过期时间
String lockKey="lockKey";
String clientId=UUID.randomUUID().toString();
Boolean result=redisTemplate.opsForValue().setIfAbsent(lockKey,clientId,10,TimeUnit.SECONDS);
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redisTemplate.execute(script,Collections.singletonList(lockKey),clientId);
1.2 redisson框架封装了上述过程
String lockKey="lockKey";
RLock redissonLock=redisson.getLock(lockKey);
try{
redissonLock.lock();
if(!result){return error;}
//核心逻辑。。。
}
finally{
redissonLock.unlock();
}
1.3 redlock
redlock全名叫做 Redis Distributed Lock
再上述实现方法中,他们都不能解决单点故障的问题,比如在一个主从模式的redis结构中,一个线程在主机中执行setnx成功,但是从库还没来得及更新主机就挂了,那么当从库成为主机后,其他线程进来加锁又可以加锁成功,从而导致并发问题
redlock使用时要求redis只有主机,且有多个主机
假设有5个完全独立的redis主服务器
-
client获取当前时间戳T1
-
client尝试按照顺序使用相同的key,value对每个redis实例加锁。
-
如果有超过一半的redis实例加锁成功,再获取一次本地时间戳T2,如果T2-T1小于等于锁的过期时间(TTL),则认为客户端加锁成功
-
如果成功获取锁,则锁的真正有效时间是 TTL减去第三步的时间差 的时间;比如:TTL 是5s,获取所有锁用了2s,则真正锁有效时间为3s(其实应该再减去时钟漂移);
-
如果加锁失败,便会开始解锁所有redis实例;可以在随机时间后重试获取锁并在同一时刻并发的把set命令发送给所有redis实例;
RedLock注意点
-
每个实例执行set命令的时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移;
-
必须半数以上的实例加锁成功才能判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况, 从而使锁失效
2、数据库实现分布式锁
要实现数据库分布式锁,首先要创建一张锁表,然后通过操作该表中的数据来实现了。
创建这样一张数据库表:
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='锁定中的方法';
2.1 通过唯一性约束来实现锁
加锁时:
insert into methodLock(method_name,desc) values (‘method_name’,‘desc’)
因为method_name唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们可以认为操作成功的那个线程获得了该方法的锁,可以执行具体内容。
释放锁:
delete from methodLock where method_name ='method_name'
存在的问题:
- 一旦数据库挂掉,会导致业务系统不可用。
- 那就搞两个数据库,数据库之间双向同步
- 这把锁没有失效时间,如果未及时释放,其他线程无法在获得锁。
- 可以做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。
- 可以在数据库表中加一个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库中可以查到的话,就直接把锁分配给它即可。
2.2 借助数据库中自带的锁来实现分布式锁。
基于MySQL的InnoDB引擎,可以使用以下方法来实现加锁操作:
public boolean lock(){
Connection.setAutoCommit(false);
while (true) {
try {
result = select * from MethodLock where methodName = 'xxxx' for update;
if (result == null) {
return false;
}
} catch (Exception e) {
}
sleep(1000);
}
returnType false;
}
在查询语句后面增加for update,数据库会在查询过程中给数据库表增加排他锁
InnoDB引擎在加锁的时候,只有通过索引进行检索的时候才会使用行级锁,否则会使用表级锁。这里我们希望使用行级锁,就要给method_name添加索引,值得注意的是,这个索引一定要创建成唯一索引,否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上)。当某条记录被加上排他锁之后,其他线程无法再在该行记录上增加排他锁。
我们可以认为获得排它锁的线程即可获得分布式锁,当获取到锁之后,可以执行方法的业务逻辑,执行完方法之后,再通过以下方法解锁:
public void unlock(){
connection.commit();
}
这里还可能存在另外一个问题,虽然我们对method_name 使用了唯一索引,并且显示使用for update来使用行级锁。但是,MySql会对查询进行优化,即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的,如果 MySQL 认为全表扫效率更高,比如对一些很小的表,它就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。如果发生这种情况就悲剧了。
还有一个问题,就是我们要使用排他锁来进行分布式锁的lock,那么一个排他锁长时间不提交,就会占用数据库连接。一旦类似的连接变得多了,就可能把数据库连接池撑爆。
原文链接: https://blog.csdn.net/lmb55/article/details/78495629
3、zookeeper实现分布式锁
3.1 基本方式
核心思想:当客户端想要获取锁时,则创建节点,使用完锁,则删除节点。
- 客户端获取锁时,在lock节点下面创建临时顺序节点(临时节点随着会话的结束会自动销毁)
- 然后获取lock下面的所有子节点,客户端获取到所有的子节点之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁。使用完锁后,将该节点删除
- 如果不是最小的,那么就去监听比自己小的那个节点的删除事件,一旦监听到删除事件,重复步骤2
三 ZooKeeper 特点
- 顺序一致性: 对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序。 这个编号也叫做时间戳——zxid(Zookeeper Transaction Id)
- 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
- 单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
- 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。
3.2 基于Curator客户端
Apache Curator是一个Zookeeper的开源客户端,它提供了Zookeeper各种应用场景(Recipe,如共享锁服务、master选举、分布式计数器等)的抽象封装
public static void main(String[] args) throws Exception {
//创建zookeeper的客户端
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.newClient("10.21.41.181:2181,10.21.42.47:2181,10.21.49.252:2181", retryPolicy);
client.start();
//创建分布式锁, 锁空间的根节点路径为/curator/lock
InterProcessMutex mutex = new InterProcessMutex(client, "/curator/lock");
mutex.acquire();
//获得了锁, 进行业务流程
System.out.println("Enter mutex");
//完成业务流程, 释放锁
mutex.release();
//关闭客户端
client.close();
}