单机锁和分布式锁及实现
作者:互联网
目录
单机锁
锁是解决并发问题的一种手段,从操作系统到应用代码都有它的身影。
- 单核时期,同一时间只能做一件事,大家依序执行:顺序执行;
- 单核性能提高了,事情的过程太慢了,核(cpu)只能干等了;
- 为了不让核闲下来,人们发明了进程,用进程来对应一个任务,由操作系统来进行调度,采取分时的方式(把cpu的时间分成很多片段),一个时间片只能执行某个进程中的指令,从用户的角度来看,任务就是并发的在处理;
- 然而一个进程中的子任务会因为某些子任务的长耗时而阻塞着,同时它无需依赖长耗时的子任务,如此是否能进一步提升进程的执行效率呢,如是发明了线程,进程中的子任务也并发执行了,为了解决数据的正确性问题,发明了互斥锁机制;
- 现在多核了,更进一步的提效,任务可以实现并行了。
并发:cpu的时间通过调度器分配给任务执行,视觉上的并行;
并行:一个任务一个核,同时执行;
锁:解决数据正确性问题
独享锁
独享锁,互斥锁:锁⼀次只能被⼀个资源所持有,其他的按照设计的策略行事,java中Synchronized而言,会进入Entry set列表等待;
共享锁
共享锁,读写锁:锁一些可以被多个资源持有
读写锁中写写是互斥的
公平锁
公平锁是指按照申请锁的顺序来获取锁;
⾮公平锁是指获取锁的顺序并不是按照申请锁的顺序,有可能后申请的⽐先申请的优先获取锁,有可能会造成优先级反转或者饥饿现象。
乐观锁
对共享数据的操作很少或者不会发生修改的。采取的策略是更新数据的时候尝试更新,如果不对继续重试,如CAS。
分布式锁
原本单机的任务因业务发展需要分解到多机执行,又或者多机上的不同任务共享一部分业务数据等,如上文所述:解决数据正确性问题产生了分布式锁概念和编程策略。
利用现成的锁
如下利用数据库提供的锁:
创建一张锁表lockTable
- 通过插入删除
当我们要锁住某个方法或资源时,我们就在该表中增加一条记录insert into lockTable(c1,c2...) values (...)
,想要释放锁的时候就删除这条记录delete from lockTable where...
,方法或资源对应的列做唯一性约束,如此多个请求插入的时候,数据库保证只有一个请求成功。
- 数据库的高可用性保证,集群化;
- 锁拥有者挂了,锁无法释放,额外增加任务超时清理锁数据;
- 锁的重入,可以增加一列标识所有者身份;
- 通过
for update
在mysql的innodb引擎中,在查询语句后面增加for update
,数据库会在查询过程中给数据库表增加排他锁。
public boolean lock(){
connection.setAutoCommit(false)
while(true){
try{
result = select * from lockTable where ... for update;
if(result==null){
return true;
}
}catch(Exception e){
}
sleep(1000);
}
return false;
}
public void unlock(){
connection.commit();
}
- 数据库的高可用性保证,集群化;
- 锁拥有者挂了,锁会自动释放;
- select语句在执行失败时一直处于阻塞状态;
- 锁的重入,可以增加一列标识所有者身份;
- 一个排他锁长时间不提交,占用数据库连接资源;
Redis
基于redis的事务性操作实现分布式锁。
加锁然后设置过期时间,防止任务挂了锁没释放:
127.0.0.1:6379> SETNX lock $uuid // 加锁
(integer) 1
127.0.0.1:6379> EXPIRE lock 10 // 10s后自动过期
(integer) 1
要保证两条命令的事务性操作(要么全成功要么全失败)
在2.6.12 之后,redis扩展了SET命令参数,用这一条命令就可以了
// 一条命令保证原子性执行
127.0.0.1:6379> SET lock $uuid EX 10 NX
OK
$uuid
:加锁时设置只有自己知道的标识,防止锁释放错误
// 锁是自己的,才释放
if(redis.get("lock") == $uuid){
redis.del("lock")
}
上述释放锁是两个命令执行的,存在事务性问题,可以通过Lua脚本来解决:
// 判断锁是自己的,才释放
if redis.call("GET",KEYS[1]) == ARGV[1]
then
return redis.call("DEL",KEYS[1])
else
return 0
end
redis 处理每一个请求是单线程执行的,在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成,这样一来,GET + DEL 之间就不会插入其它命令了,要么DEL成功,要么就是没有DEL,客户端添加确认成功与否逻辑即可
锁的过期时间不好评估问题
可以设计这样的方案:加锁时,先设置一个过期时间,然后我们开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。
库Redisson已封装实现
redis集群化可以参考Redis的作者提出一种解决方案Redlock。
zookeeper
基于zookeeper临时有序节点可以实现分布式锁。
过程如下:
- 客户端 1 和 2 都尝试创建临时节点,例如 /lock;
- 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败;
- 客户端 1 操作共享资源;
- 客户端 1 删除 /lock 节点,释放锁;
使用 Zookeeper,无法保证进程 GC、网络延迟异常场景下的安全性。
客户端与zk服务器维护一个Session,这个Session会依赖客户端定时心跳来维持连接,如果zk长时间收不到客户端的心跳,就认为这个 Session 过期了,会把这个临时节点删除。
参考:
https://blog.csdn.net/wuzhiwei549/article/details/80692278
https://mp.weixin.qq.com/s/JwXtmEzlHfkMhaoCWzIWJQ
标签:执行,加锁,单机,lock,锁及,redis,任务,分布式,客户端 来源: https://blog.csdn.net/m0_60725291/article/details/119480664