zookeeper应用实战之分布式锁

1. 什么是分布式锁?

我们先来看这样一个场景,如下图所示,两个用户同时去抢购秒杀商品,当秒杀服务同时收到秒杀请求时,都去进行库存扣减,此时在没有做任何处理的情况下,就会导致库存数量变成负数从而导致超卖现象。

这种情况下如果是单体项目,我们一般会选择加锁的方式来避免并发的问题。但是在分布式场景中,采用传统的锁并不能解决跨进程并发的问题,所以需要引入一个分布式锁,来解决多个节点之间的访问控制。

image-20220227201628324

2. Zookeeper如何实现分布式锁

实现分布式的方式有很多种,本文主要讲述如何使用zookeeper实现分布式锁。我们可以基于zookeeper的两种特性来实现分布式锁,首先我们来看第一种:

2.1 唯一节点特性

我们可以基于唯一节点特性来实现分布式锁的操作,如下图所示。多个应用程序去抢占锁资源时,只需要在指定节点上创建一个 /Lock 节点,由于Zookeeper中节点的唯一性特性,使得只会有一个用户成功创建 /Lock 节点,剩下没有创建成功的用户表示竞争锁失败。

image-20220227203239217

这种方法虽然能达到目的,但是会有一个问题,如下图所示,假设有非常多的节点需要等待获得锁,那么等待的方式自然是使用watcher机制来监听/lock节点的删除事件,一旦发现该节点被删除说明之前获得锁的节点已经释放了锁,那么此时剩下的B、C、D节点会同时收到删除事件从而去竞争锁,这个过程会产生惊群效应。

image-20220227203406701

什么是“惊群效应”呢?简单来说就是如果存在许多的客户端在等待获取锁,当成功获取到锁的进程释放该节点后,所有处于等待状态的客户端都会被唤醒,这个时候zookeeper会在短时间内发送大量子节点变更事件给所有待获取锁的客户端,然后实际情况是只会有一个客户端获得锁。如果在集群规模比较大的情况下,会对zookeeper服务器的性能产生比较的影响。

2.2 有序节点

为了解决惊群效应,我们可以采用Zookeeper的有序节点特性来实现分布式锁。

如下图所示,每个客户端都往指定的节点下注册一个临时有序节点,越早创建的节点,节点的顺序编号就越小,那么我们可以判断子节点中最小的节点设置为获得锁。如果自己的节点不是所有子节点中最小的,意味着还没有获得锁。这个的实现和前面单节点实现的差异性在于,每个节点只需要监听比自己小的节点,当比自己小的节点删除以后,客户端会收到watcher事件,此时再次判断自己的节点是不是所有子节点中最小的,如果是则获得锁,否则就不断重复这个过程,这样就不会导致羊群效应,因为每个客户端只需要监控一个节点。

image-20220227203638313

使用有序节点实现分布式锁的流程大致如下:

image-20220227203716981

3. Curator实现分布式锁

在日常开发种,我们无需自己去实现分布式锁,只需要使用Curator即可实现分布式锁。

为了实现分布式锁,我们先演示一个存在并发异常的场景。

3.1 商品抢购场景

SQL

DROP TABLE IF EXISTS `goods_stock`;
CREATE TABLE `goods_stock`  (
  `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  `goods_no` int(11) NOT NULL COMMENT '商品编号',
  `stock` int(11) NULL DEFAULT NULL COMMENT '库存',
  `isActive` smallint(6) NULL DEFAULT NULL COMMENT '是否上架(1上,0不是)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

整个项目采用spring boot+mybatis-plus框架,代码一键生成。主要编写controller层代码即可:

这个抢购接口乍一看好像没啥问题,但实际上是存在问题的,因为他不具有原子性,在高并发场景下会造成数据多扣减。

我们可以使用jmeter对这个接口进行压测,用1500个线程,库存数量设置成100,监视数据库中库存的变化发现,整个库存变化过程是非常混乱的。可能一会数字变小,但是一会又变大了…

@RestController
@RequestMapping("/goods-stock")
public class GoodsStockController {

    @Autowired
    private IGoodsStockService goodsStockService;


    @GetMapping("/{goodsNo}")
    public String purchase(@PathVariable("goodsNo") Integer goodsNo) throws Exception {
        QueryWrapper<GoodsStock> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("goods_no", goodsNo);
        GoodsStock goodsStock = goodsStockService.getOne(queryWrapper);
        Thread.sleep(new Random().nextInt(1000)); //增加问题出现的频率
        if (goodsStock == null) {
            return "指定商品不存在";
        }
        if (goodsStock.getStock().intValue() < 1) {
            return "库存不够";
        }
        goodsStock.setStock(goodsSto
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值