目录

分布式锁

在多线程并发读写共享数据及不可预知的线程调度情况下, 为了避免造成数据不一致的问题. 我们通常会采用一定的方式来协调这些并发线程的行为. 最常用的方式就是锁.

锁其实是一种标志, 或一种设计思想, 他表示一个共享资源当前被某单位占有, 其他单位不允许操作.

分布式锁

在同一个jvm中, 我们可以用 synchronized, Lock 等方式来进行线程的同步;但在分布式的集群环境中, 操作同一数据的并发线程可能在不同节点上. 于是, 我们需要分布式锁.

场景

  • 防止库存超卖:每个下单流程都会对现有库存加锁, 扣减库存后释放锁。

实现方式

Redis实现

Redis的网络模块是单线程的, 所以不需要考虑并发安全性, 但其他处理模块仍然使用多线程以提高处理效率.

我们通过setnx操作向redis中设置一个key来代表锁标志; 谁set成功,锁由谁持有,这里value我们不关心。

防死锁

为防止某申请者set锁标志后宕机造成死锁问题, 需要给key设置有效期. 有以下解决方案:

  • 在set完key之后,直接设置key的有效期

    由redis来删除key释放锁

  • 在value中注明超时时间(存在可忽略不计的小问题)

    其他申请者通过get发现时间已到之后可以执行删除key操作.然后可以使用GETSET

Zookeeper实现

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。

ZooKeeper就像是我们的电脑文件系统,我们可以在d盘中创建文件夹a,并且可以继续在文件夹a中创建文件夹a1,a2。文件系统有什么特点?那就是同一个目录下文件名称不能重复,同样ZooKeeper也是这样的。在ZooKeeper所有的节点,也就是文件夹称作Znode,而且这个Znode节点是可以存储数据的。

我们可以通过” create /zkjjj nice"来创建一个节点,这个命令就表示,在跟目录下创建一个zkjjj的节点,值是nice,这里value我们不关心。

通过在zookeeper中创建节点来代表锁标志。谁创建成功,锁由谁持有。 其他申请者创建失败,就只能监听该znode。持有者处理完业务后, 删除了该znode,监听者们会得到通知,然后尝试去创建znode,这个尝试过程是并发进行的的。

防死锁

持有者创建znode之后挂掉,导致znode未删除,造成死锁。

所以,需要用到临时性节点。临时性节点的特性是zookeeper客户端断开,则节点会被自动删除,即锁被释放。

临时顺序性节点

惊群效应: 往一群鸽子中间扔一块食物,虽然最终只有一个鸽子抢到食物,但所有鸽子都会被惊动来争夺,却没有抢到…;在分布式系统中就是,znode被删除后,所有监听者都来尝试创建znode,但最终只有一个能创建成功。

假设100个服务器同时发来请求,这个时候会在/zkjjj节点下创建100个临时顺序性节点/zkjjj/000000001,/zkjjj/000000002,一直到/zkjjj/000000100,这个编号就等于是已经给他们设置了获取锁的先后顺序了。 当001节点处理完毕,删除节点后,002收到通知,去获取锁,开始执行,执行完毕,删除节点,通知003~以此类推。避免了一次性通知99个节点,造成惊群效应。

生产方案

网上有很多讨论分布式锁实现方案的文章, 我们不推荐自己来实现分布式锁, 用开源的会比较好, 这里推荐一个Redisson.其提供了很多功能, 分布式锁只是其中一个.

非spring boot需要自己配置redisson对象.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class Demo {
    @Autowired
    private Redisson redisson;// spring boot中不需要手动创建对象, 注入一个即可

    public void test() throws InterruptedException {
        // 这个生成key的规则所有节点要统一
        RLock lock = redisson.getLock("anyLock");

        //最常见的使用方法 lock.lock(),一直等待,直到获取锁.
        //支持过期自动解锁, 不怕死锁
        lock.lock(10, TimeUnit.SECONDS);

        //尝试加锁,最多等待100秒,超时未成功返回失败
        //上锁以后10秒自动解锁,不怕死锁
        boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);

        //...
        lock.unlock();//完成业务逻辑后解锁, 减少加锁时间, 提高并发度
    }
}

问题

在防止库存超卖的场景中,多个用户同时下单时,会基于分布式锁串行化处理。如果是秒杀之类的高并发场景,这样的处理效率是无法接受的。

高并发场景下的优化

优化思路是分段锁,类似concurrentHashMap的思路。比如把1000个库存拆成20个分段,在数据库里将原来的一个库存字段拆成20个库存字段。这样,每一个下单请求可以锁一个库存字段。这样并发度就提升至20倍。不过这个优化方案有很多缺点,不再赘述。秒杀场景下有其他更好方案防止库存超卖,使用分布式锁效率不高。

References