传统的Synchronized以及Lock锁都是基于JVM的,由于在分布式系统中,会有多个Web容器同时运行,导致多个Web容器内部的传统锁已经不存在互斥性了

Zookeeper实现分布式锁的原理

Zookeeper是一种分布式协调服务,Zookeeper实现分布式锁的原理就是利用临时有序结点

  1. 客户端在指定结点下创建临时有序结点
  2. 如果当前临时有序结点的序号是最小的,则获取锁成功
  3. 如果当前临时有序结点的序号不是最小的,则它监听比它小一号的结点
  4. 当它监听的结点被修改或删除时,那么它自己就会重新判断自己是不是最小的结点,重复到第2步

有序实现了锁的功能,而临时的目的就是为了防止死锁

Zookeeper实现分布式锁

SpringBoot工程

实现目标功能:简单的秒杀功能,多线程同时秒杀指定数量的商品,因为是模式,所以将库存等数据存储在本地,也就是使用Map集合来实现

安装Zookeeper:

1、导入依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
   <groupId>org.apache.zookeeper</groupId>
   <artifactId>zookeeper</artifactId>
   <version>3.6.0</version>
   <exclusions>
      <exclusion>
         <artifactId>slf4j-api</artifactId>
         <groupId>org.slf4j</groupId>
      </exclusion>
      <exclusion>
         <artifactId>slf4j-log4j12</artifactId>
         <groupId>org.slf4j</groupId>
      </exclusion>
      <exclusion>
         <artifactId>log4j</artifactId>
         <groupId>log4j</groupId>
      </exclusion>
   </exclusions>
</dependency>

<dependency>
   <groupId>org.apache.curator</groupId>
   <artifactId>curator-recipes</artifactId>
   <version>4.2.0</version>
   <exclusions>
      <exclusion>
         <artifactId>slf4j-api</artifactId>
         <groupId>org.slf4j</groupId>
      </exclusion>
   </exclusions>
</dependency>

2、配置Zookeeper

@Configuration
public class ZkConfig {

    @Bean
    public static CuratorFramework cf(){
        RetryPolicy retryPolicy = new BoundedExponentialBackoffRetry(3000,6000,2);
        CuratorFramework cf = CuratorFrameworkFactory.builder().connectString("192.168.111.111:2181").retryPolicy(retryPolicy).build();
        cf.start();
        return cf;
    }

}

3、实现

@RestController
@RequestMapping("/zk/secKill")
public class ZookeeperSecKillController {

    @Resource
    private CuratorFramework cf;

    // 订单数量
    private static Map<String, Integer> orderMap = new HashMap<>();

    // 库存数量
    private static Map<String, Integer> stockMap = new HashMap<>();

    static {
        orderMap.put("snacks", 0); // 初始订单数量为0
        stockMap.put("snacks", 1000); // 初始库存数量为1000
    }

    // 进行秒杀
    @RequestMapping("/sk/{itemName}")
    public String sk(@PathVariable String itemName) throws Exception {

        // 创建锁
        InterProcessMutex lock = new InterProcessMutex(cf, "/lock/" + itemName);

        // 1、查询商品库存,
        Integer stock = stockMap.get(itemName);

        // 2、判断库存是否充足
        if (stock < 0) {
            return itemName + "已经被抢完了!!!";
        }

        // 如果获取锁成功(如果2秒获取不到锁,就放弃)
        if (lock.acquire(2000, TimeUnit.MILLISECONDS)) {
            // 3、创建订单
            orderMap.put(itemName, orderMap.get(itemName) + 1);
            Thread.sleep(200); // 模拟操作数据库,延时一会

            // 4、减少库存
            stockMap.put(itemName, stockMap.get(itemName) - 1);
            Thread.sleep(200);

            // 释放锁
            lock.release();
        }

        // 5、下单成功
        return "下单成功";
    }

    // 查询当前商品数量
    @RequestMapping("/info/{itemName}")
    public String info(@PathVariable String itemName) {
        return itemName + " 商品一共下单 " + orderMap.get(itemName) + " 份, 库存还剩 " + stockMap.get(itemName);
    }

}

4、浏览器访问http://localhost:8080/zk/secKill/info/snacks,会打印出当前商品的剩余数量和库存数量

5、使用压力测试工具进行测试

链接:https://pan.baidu.com/s/1KwAas_fS2H3NZkLV63DeHQ 提取码:lc8z

下载压力测试工具,进入到bin目录下,打开命令提示符,输入命令,进行大量并发秒杀访问

ab -n 2000 -c 500 http://localhost:8080/zk/secKill/sk/snacks
# ab -n 请求数 -c 并发数 访问的路径

然后再次访问http://localhost:8080/zk/secKill/info/snacks,查询当前商品剩余数量,发现被秒杀掉的商品不多,2000条请求,但是只秒杀掉不到10件,那是因为要保证秒杀的安全性,就要导致一些请求抢不到锁,无法秒杀成功

Redis实现分布式锁的原理

向Redis中添加数据时,使用setnx命令

  • 如果不存在此key,就添加成功,也就是获取锁成功
  • 如果此key已经存在了,则不做任何操作,也就是获取锁失败

为了解决死锁的问题,使用setex命令

  • setex命令也就是设置生存时间,如果发生死锁,那么等生存时间一到,数据被删除,也就会释放锁资源

在Java当中,setnx和setex只需一条命令就能实现

redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);

Redis实现分布式锁

SpringBoot工程

实现目标功能:简单的秒杀功能,多线程同时秒杀指定数量的商品,因为是模式,所以将库存等数据存储在本地,也就是使用Map集合来实现

1、导入依赖

  • 导入一个web的依赖,因为要在浏览器上测试
  • 导入一个redis的依赖,毫无疑问使用Redis实现
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

2、配置redis信息

spring:
  redis:
    host: 192.168.111.111
    password:

3、实现

@RestController
@RequestMapping("/redis/secKill")
public class SecKillController {

    @Resource
    private StringRedisTemplate redis;

    // 订单数量
    private static Map<String, Integer> orderMap = new HashMap<>();

    // 库存数量
    private static Map<String, Integer> stockMap = new HashMap<>();

    static {
        orderMap.put("snacks", 0); // 初始订单数量为0
        stockMap.put("snacks", 1000); // 初始库存数量为1000
    }

    // 进行秒杀
    @RequestMapping("/sk/{itemName}")
    public String sk(@PathVariable String itemName) throws InterruptedException {

        // 1、查询商品库存,
        Integer stock = stockMap.get(itemName);

        // 2、判断库存是否充足
        if (stock < 0) {
            return itemName + "已经被抢完了!!!";
        }

        // 如果获取锁成功
        if (redis.opsForValue().setIfAbsent("SK" + itemName, "1", 3000, TimeUnit.MILLISECONDS)) {
            // 3、创建订单
            orderMap.put(itemName, orderMap.get(itemName) + 1);
            Thread.sleep(200); // 模拟操作数据库,延时一会

            // 4、减少库存
            stockMap.put(itemName, stockMap.get(itemName) - 1);
            Thread.sleep(200);

            // 释放锁
            redis.delete("SK" + itemName);
        }

        // 5、下单成功
        return "下单成功";
    }

    // 查询当前商品数量
    @RequestMapping("/info/{itemName}")
    public String info(@PathVariable String itemName) {
        return itemName + " 商品一共下单 " + orderMap.get(itemName) + " 份, 库存还剩 " + stockMap.get(itemName);
    }

}

4、浏览器访问http://localhost:8080/redis/secKill/info/snacks,会打印出当前商品的剩余数量和库存数量

5、使用压力测试工具进行测试

链接:https://pan.baidu.com/s/1KwAas_fS2H3NZkLV63DeHQ 提取码:lc8z

下载压力测试工具,进入到bin目录下,打开命令提示符,输入命令,进行大量并发秒杀访问

ab -n 2000 -c 500 http://localhost:8080/redis/secKill/sk/snacks
# ab -n 请求数 -c 并发数 访问的路径

然后再次访问http://localhost:8080/redis/secKill/info/snacks,查询当前商品剩余数量,发现被秒杀掉的商品不多,2000条请求,但是只秒杀掉不到10件,那是因为要保证秒杀的安全性,就要导致一些请求抢不到锁,无法秒杀成功,不过这并不影响功能的整体实现