分布式锁的实现方式简介

方式一:数据库方式(了解,基本不会用)

DROP TABLE IF EXISTS `method_lock`;
CREATE TABLE `method_lock` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
  `method_name` varchar(64) NOT NULL COMMENT '锁定的方法名',
  `state` tinyint NOT NULL COMMENT '1:未分配;2:已分配',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `version` int NOT NULL COMMENT '版本号',
  `PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_method_name` (`method_name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

每次先去查询,如果此方法已经被分配了,则获取失败,否则更新state为已分配,并且版本号递增,如果更新成功则获取锁成功,否则失败

缺点:

    1、这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
    2、这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
    3、这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
    4、这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

解决方案:
     1、数据库是单点?搞两个数据库,数据之前双向同步。一旦挂掉快速切换到备库上。
     2、没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
     3、非阻塞的?搞一个while循环,直到insert成功再返回成功。
     4、非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。

 

方式二:缓存方式(Redis)(高性能)

SET method_key my_random_value NX PX 30000 

也就是通过redis设置同一个key,如果设置成功,则说明抢到锁了,为了防止抢到锁的客户端挂掉或者删除key的时候出异常,我们会给这个key过期时间,否则可能会出现锁永远无法释放的情况

(当然因为某些场景可能我们设置过期时间内占用锁的任务并没有执行完,这个时候我们也可以给这个进程加一个后台线程,当过期时间快到时,我们给它延长,当任务执行完,进程退出,后台线程被杀死)

【这个思想其实就是单线程的思想,想一下就知道了,虽然我们并发访问不同tomcat,但是执行的时候汇总到一个redis中,顺序执行(至于redis集群是必要的,试想一下单机redis如果挂掉了,那么系统分布式锁就无法用了,那岂不是完蛋了,但是这有问题,即使redis是主从架构,当我们向master写入key后,master正准备向其他从机同步的时候挂掉了,这样锁就没同步过去,另外一个线程就可以获取这个锁了;

这样的问题可以通过引入zookeeper来实现,因为zk半数以上节点写入成功了,才会返回客户端写入成功,即使主节点挂掉了,zk的算法机制回选取获取到数据的follower当主节点,就解决了这个问题,不过使用redis引入zk,性能就降低了,还不如直接使用zk呢,因此使用redis分布式锁,为了高效,可能会出现这样的问题,不过这样问题出现的概率极低,而且即使出现了,也就是多一个线程,是可容忍的(当然如果涉及到了金钱就不要使用redis分布式方式加锁了,要使用zk,因为涉及到钱不可以出错)

)】

缺点:

在这种场景(主从结构)中存在明显的竞态:
    客户端A从master获取到锁,
    在master将锁同步到slave之前,master宕掉了。
    slave节点被晋级为master节点,
    客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!

redis实现参考网址:https://www.cnblogs.com/linjiqin/p/8003838.html 

 

方式三:基于zookeeper实现(高可靠性)

zookeeper的znode有4种类型:

1.持久节点(默认):创建节点的客户端与zk断开后节点仍存在

2.持久顺序节点:根据创建的时间顺序给节点编号排序,仍然是创建节点的客户端与zk断开后节点仍存在

3.临时节点:与持久节点相反,建节点的客户端与zk断开后节点就被删除

4.临时顺序节点:根据创建的时间顺序给节点编号排序,建节点的客户端与zk断开后节点就被删除

 

基于zk的分布式锁使用的就是临时顺序节点:

zk创建一个持久的节点,用于实现分布式锁,客户端1来的时候创建znode,并且判断它是否是最靠前的节点,如果是则获取锁,如果不是则向仅比它靠前的节点建立watch,这样当任何一个节点挂掉之后就会立即通知watch它的客户端,然后这个客户端仍然判断当前它是不是最靠前的节点,如果是则获取锁,如果不是则则向仅比它靠前的节点建立watch,同上了;

这样即使某个客户端突然宕机,也无所谓了,因为它宕机了,它在zk的节点就会被删掉,这样watch它的客户端就会收到通知。

缺点:

  因为需要不断创建和删除节点,因此性能并不是特别高; 

  其实,使用Zookeeper也有可能带来并发问题,只是并不常见而已。考虑这样的情况,由于网络抖动,客户端可ZK集群的session连接断了,那么zk以为客户端挂了,就会删除临时节点,这时候其他客户端就可以获取到分布式锁了。就可能产生并发问题。这个问题不常见是因为zk有重试机制,一旦zk集群检测不到客户端的心跳,就会重试,Curator客户端支持多种重试策略。多次重试之后还不行的话才会删除临时节点。(所以,选择一个合适的重试策略也比较重要,要在锁的粒度和并发之间找一个平衡。)

 

参考来源:https://blog.csdn.net/wuzhiwei549/article/details/80692278

posted @ 2020-12-24 22:51  程序杰杰  阅读(103)  评论(0编辑  收藏  举报