文章目录
-
- 1.什么是分布式锁
- 2.分布式锁应该具备哪些条件
- 3.分布式锁主流的实现方案
- 4.未添加分布式锁存在的问题
-
- 4.1测试未添加分布式锁的代码
-
- 通过jmeter发送请求
- 4.2 添加线程同步锁
-
- 集群部署
-
- 配置nginx
- 修改jmeter端口号
- 4.3 使用redis的setnx命令实现分布式锁
-
- 解决办法
- 4.4 使用try、finally优化
- 4.5 添加分布式锁的过期时间
- 4.6 解决分布式锁命令的原子性问题
- 4.7 把线程ID做为分布式锁的value
- 5.使用redisson实现分布式锁
-
- 5.1 添加 Redisson 依赖
- 5.2 获取 Redisson 客户端实例
- 5.3 获取分布式锁
- 5.4 Redisson分布式锁的卖现原理图
- 6 Redisson相关锁介绍
-
- 6.1 可重入锁
- 6.2 联锁概述
- 6.3 RedLock
1.什么是分布式锁
问题描述: 随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的 JavaAPI 并不能提供分布式锁的能力,为了解决这个问题就需要一种跨IV的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
与分布式锁相对就的是单体结构中的锁 (单机锁) ,我们在写多线程程序时,避免同时操作一个共享变量产生数据问题,通常会使用一把锁来互斥以保证共享变量的正确性,其使用范围是在同一个JVM进程中。如果换做是不同机器上的MM进程,需要同时操作一个共享资源,如何互斥呢?现在的业务应用通常是微服务架构,这也意味着一个应用会部署多个进程,多个进程如果需要修改MySQL中的同一行记录,为了避免操作乱序导致脏数据,此时就需要引入分布式锁了。
2.分布式锁应该具备哪些条件
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁与释放锁
- 高性能的获取锁与释放锁
- 具备可重入特(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
- 具备锁失效机制,即自动解锁,防止死锁
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败
3.分布式锁主流的实现方案
- 1.基于缓存(redis等)
- 2.基于zookeeper
每一种分布式锁解决方案都有各自的优缺点
- 1.性能:redis最高
- 2.可靠性:zookeeper最高
这里我们就基于redis实现分布式锁。
4.未添加分布式锁存在的问题
实现秒杀下单减库存案例:
创建springboot项目,导入redis依赖,在yml中进行redis的配置:
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置:
spring:
redis :
database:0
host:localhost
port:6379
4.1测试未添加分布式锁的代码
操作redis
# 在redis 中设置一个stock等于10
set stock 0
get stock
package cn.js;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class Indexcontroller {
@Resource
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deductstock")
public String deductstock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realstock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realstock + "");
System.out.println("扣减成功,剩余库存:" + realstock);
} else {
System.out.println("扣减失败,库存不足");
}
return "end";
}
}
通过jmeter发送请求
通过命令再次查看redis
我们先清空redis再试一次
再次通过jmeter发送请求
两次结果
研究代码发现存在线程安全问题,接下来可以加个同步代码锁synchronized进行优化
4.2 添加线程同步锁
package cn.js;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class Indexcontroller {
@Resource
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deductstock")
public String deductstock() {
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realstock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realstock + "");
System.out.println("扣减成功,剩余库存:" + realstock);
} else {
System.out.println("扣减失败,库存不足");
}
}
return "end";
}
}
目前我们的环境是单体服务,可以通过 synchronized JVM锁进行解决,但是如果是集群部署的话,synchronized锁是没有用的
集群部署
配置nginx
打开nginx中的nginx.conf文件
在.idea的workspace.xml中配置
<option name="configurationTypes">
<set>
<option value="springBootApplicationconfigurationType"/>
</set>
</option>
修改jmeter端口号
可以看到集群部署会出现超卖问题
重启IDEA
最后使用imeter工具,模拟高并发场景进行压力测试,查看控制台打印日志,发现还是出现库存量一样的数据,说明超卖问题还是存在。
分析得出:在分布式环境下synchronized是不起作用的,因为一个synchronized只在一个tomcat的iym进程内有效,在一个分布式系统,如何解决并发的资源争抢问题呢?
4.3 使用redis的setnx命令实现分布式锁
解决办法
可以通过redis的setnx命令的互斥性:
package cn.js;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org