传统的Synchronized以及Lock锁都是基于JVM的,由于在分布式系统中,会有多个Web容器同时运行,导致多个Web容器内部的传统锁已经不存在互斥性了
Zookeeper实现分布式锁的原理
Zookeeper是一种分布式协调服务,Zookeeper实现分布式锁的原理就是利用临时有序结点
- 客户端在指定结点下创建临时有序结点
- 如果当前临时有序结点的序号是最小的,则获取锁成功
- 如果当前临时有序结点的序号不是最小的,则它监听比它小一号的结点
- 当它监听的结点被修改或删除时,那么它自己就会重新判断自己是不是最小的结点,重复到第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件,那是因为要保证秒杀的安全性,就要导致一些请求抢不到锁,无法秒杀成功,不过这并不影响功能的整体实现