微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

单机锁和分布式锁及实现

目录

单机锁

锁是解决并发问题的一种手段,从操作系统到应用代码都有它的身影。

  1. 单核时期,同一时间只能做一件事,大家依序执行:顺序执行;
  2. 单核性能提高了,事情的过程太慢了,核(cpu)只能干等了;
    • 为了不让核闲下来,人们发明了进程,用进程来对应一个任务,由操作系统来进行调度,采取分时的方式(把cpu的时间分成很多片段),一个时间片只能执行某个进程中的指令,从用户的角度来看,任务就是并发的在处理;
    • 然而一个进程中的子任务会因为某些子任务的长耗时而阻塞着,同时它无需依赖长耗时的子任务,如此是否能进一步提升进程的执行效率呢,如是发明了线程,进程中的子任务也并发执行了,为了解决数据的正确性问题,发明了互斥锁机制
  3. 现在多核了,更进一步的提效,任务可以实现并行了。

并发:cpu的时间通过调度器分配给任务执行,视觉上的并行;
并行:一个任务一个核,同时执行;
锁:解决数据正确性问题

独享锁

独享锁,互斥锁:锁⼀次只能被⼀个资源所持有,其他的按照设计的策略行事,java中Synchronized而言,会进入Entry set列表等待;

共享锁

共享锁,读写锁:锁一些可以被多个资源持有

读写锁中写写是互斥的

公平锁

公平锁是指按照申请锁的顺序来获取锁;
⾮公平锁是指获取锁的顺序并不是按照申请锁的顺序,有可能后申请的⽐先申请的优先获取锁,有可能会造成优先级反转或者饥饿现象。

乐观锁

对共享数据的操作很少或者不会发生修改的。采取的策略是更新数据的时候尝试更新,如果不对继续重试,如CAS。

分布式锁

原本单机的任务因业务发展需要分解到多机执行,又或者多机上的不同任务共享一部分业务数据等,如上文所述:解决数据正确性问题产生了分布式锁概念和编程策略。

利用现成的锁

如下利用数据库提供的锁:
创建一张锁表lockTable

  • 通过插入删除
    当我们要锁住某个方法或资源时,我们就在该表中增加一条记录insert into lockTable(c1,c2...) values (...),想要释放锁的时候就删除这条记录delete from lockTable where...方法或资源对应的列做唯一性约束,如此多个请求插入的时候,数据库保证只有一个请求成功。
  • 数据库的高可用性保证,集群化;
  • 锁拥有者挂了,锁无法释放,额外增加任务超时清理锁数据;
  • 锁的重入,可以增加一列标识所有者身份;
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的作者提出一种@R_502_6280@案Redlock。

zookeeper

基于zookeeper临时有序节点可以实现分布式锁。
过程如下:

  1. 客户端 1 和 2 都尝试创建临时节点,例如 /lock;
  2. 假设客户端 1 先到达,则加锁成功,客户端 2 加锁失败;
  3. 客户端 1 操作共享资源;
  4. 客户端 1 删除 /lock 节点,释放锁;

使用 Zookeeper,无法保证进程 GC、网络延迟异常场景下的安全性。

客户端与zk服务器维护一个Session,这个Session会依赖客户端定时心跳来维持连接,如果zk长时间收不到客户端的心跳,就认为这个 Session 过期了,会把这个临时节点删除

参考:
https://blog.csdn.net/wuzhiwei549/article/details/80692278
https://mp.weixin.qq.com/s/JwXtmEzlHfkMhaoCWzIWJQ

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐