面试专题之——分布式锁

什么是分布式锁?为什么要用分布式锁?

在使用本地线程锁时,本质上是在内存中存放一个标记,如果该标记中有线程存在则表示获得了锁,删除该线程则释放锁。但是在分布式系统中,每个微服务都不是同一个jvm,它们的内存是不共享的,所以它们的锁也是不共享的,自然而然就无法实现互斥锁了。

所以这就需要使用到分布式锁了,分布式锁本质就是让那个标记所有微服务都可见,并且共用一个,这样就能实现互斥。

实现分布式锁的方式

Redis如何实现分布式锁?

分布式锁一般都是可重入锁,这样某个业务调用的子函数也能获取到锁,Redis实现可重入锁需要使用hash类型的数据,key为锁名称,hash的key作为当前的线程id,hash的value值作为重入次数,为了防止死锁还要设置一个过期时间。

获取锁过程:

判断锁是否存在

  • 存在:

    1、根据线程id判断当前锁是否为自己的,不是自己的获取锁失败,是自己的则下一步

    2、hash的value值加1,获取锁成功

    3、设置有效期

  • 不存在:

    1、用hset添加键值对获取锁

    2、设置有效期

释放锁过程:

判断当前锁是否存在,并且是不是自己的

  • 存在并且是自己的:如果冲入次数不为0,则对其减去1并重置过期时间,直到为0才进行删除锁操作,也即是释放锁
  • 不存在或不是自己的:直接返回,不做任何操作

出现的问题:

1、获取锁与释放锁都需要多条命令完成,导致操作不是原子性的,这就需要使用lua脚本来实现原子性了。当然还有一个更简便的方法,就是直接使用redisson,它集成了这些功能,通过它的trylock方法来获取锁

2、如果任务不能在超时时间内完成,那么锁就可能被别人获取,导致线程安全问题

解决方式:

1、创建一个定时任务,如果任务没有执行完成就对分布式锁进行续期

2、配合事务使用,只要超时直接进行数据回滚

3、redis主从复制集群中,主节点加锁成功,但还没来得及将数据同步给其他从节点就宕机了,这时哨兵机制会选出新的主节点,但这个主节点没有刚刚加锁的信息,这就导致这个锁失效了

解决方法:

1、Redisson框架为了解决这个问题,提供了一个类RedissonRedLock,可以实现搭建多个相同的主从集群,加锁时同时在多个相同主节点上添加,如果有n/2 + 1个主节点加锁成功,那么本次加锁成功。但是这带来两个问题,所以这种方式用的其实并不多:

1、需要搭建多套环境,成本高

2、在多个相同主节点上同时加锁所需要的时间成本高

2、鱼和熊掌不可兼得,CAP定理中CP和AP是无法兼得的,如果更需要保证数据一致性直接用zookeeper实现分布式锁即可。

但其实,我们在绝大多数分布式业务场景中,使用redis实现分布式锁就够了,数据不一致问题可以通过最终一致性方案解决,像一些不重要业务直接舍弃这条数据都行,但是如果并发太高导致系统不可用了,对用户伤害才是最大的。

优点:性能好,并发能力强,实现简单

缺点:可靠性有争议,极端情况下会出现锁失效的问题,如果对数据安全要求比较高不建议使用

zookeeper如何实现分布式锁?

zookeepr中有一个临时有序节点,可以通过它来实现分布式锁。

获取锁:

1、在/lock目录下创建一个节点

2、判断当前节点序号是否为最小

  • 最小:判定获取锁成功
  • 非最小:对前一个节点添加事件监听,用于判断前一个节点是否删除(释放锁),然后当前节点进入等待状态,等前一个节点删除后,当前节点则获取到锁

释放锁:删除当前节点即可

优点:实现最简单,数据安全性高

缺点:性能略微差与Redis

Mysql实现分布式锁

利用了数据库自带的排他锁实现

实现步骤:

1、开启事务

2、select查询分布式锁相关数据,并加上for update实现排他查询(获取锁)

3、执行业务

4、提交事务(释放锁)

优点:实现极其简单

缺点:性能很差、可能出现单点故障的问题