1、什么是分布式锁?
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度。而这个分布式协调技术的核心就是来实现这个分布式锁。
2、为什么使用分布式锁?
我们在java开发工作中,如果需要对某一个共享变量进行多线程同步访问的时候,可以使用我们学到的Java多线程进行处理。
后来业务发展,需要做集群,一个应用需要部署到几台机器上然后做负载均衡,大致如下图:
图片来源:夏目 "
上图可以看到,变量A存在JVM1、JVM2、JVM3三个JVM内存中(这个变量A主要体现是在一个类中的一个成员变量,是一个有状态的对象,例如:UserController控制器中的一个整形类型的成员变量),如果不加任何控制的话,变量A同时都会在JVM分配一块内存,三个请求发过来同时对这个变量操作,显然结果是不对的!即使不是同时发过来,三个请求分别操作三个不同JVM内存区域的数据,变量A之间不存在共享,也不具有可见性,处理的结果也是不对的!
如果我们业务中确实存在这个场景的话,我们就需要一种方法解决这个问题!
分布式锁的作用:在解决单个服务的线程同步安全中,我们使用的synchronized等java的方式加锁。但是在面临多个服务去访问一个公共资源时,是要保证服务层面的同步安全性,synchronized等java的加锁方式就不解决不了问题了。
这是要解决服务的同步性问题。
3、分布式锁应该具备哪些条件
1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;
2、高可用的获取锁与释放锁;
3、高性能的获取锁与释放锁;
4、具备可重入特性;
5、具备锁失效机制,防止死锁;
6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。
4、Redis分布式锁
使用Redis分布式锁的原理:首先,redis是单线程的,这是前提条件。redis中有值超时的设置以及重复值不可插入并返回false的功能。就能保证在一个进程执行了一个访问公共资源的方法时,往redis中设置了一个标识,这个过程叫做加锁。等另一个进程同样操作这个方法时,再去加锁,就发现已经有了,就等待,并不停的尝试加锁。
分布式锁实现的三个核心要素:
- 加锁
最简单的方法是使用 setnx 命令。key 是锁的唯一标识,按业务来决定命名。比如想要给一种商品的秒杀活动加锁,可以给 key 命名为 “lock_sale_商品ID” 。而 value 设置成什么呢?我们可以姑且设置成 1。加锁的伪代码如下:
setnx(lock_sale_商品ID,1)
当一个线程执行 setnx 返回 1,说明 key 原本不存在,该线程成功得到了锁;当一个线程执行 setnx 返回 0,说明 key 已经存在,该线程抢锁失败。
- 解锁
有加锁就得有解锁。当得到锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式是执行 del 指令,伪代码如下:
del(lock_sale_商品ID)
释放锁之后,其他线程就可以继续执行 setnx 命令来获得锁。
- 锁超时
锁超时是什么意思呢?如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住(死锁),别的线程再也别想进来。所以,setnx 的 key 必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一定时间后自动释放。setnx 不支持超时参数,所以需要额外的指令,伪代码如下:
expire(lock_sale_商品ID, 30)
5、Redis分布式锁示例
创建springboort项目,使用nginx做代理,开启redis服务。
1、首先需要加入需要的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.4</version>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、 项目中的application.properties配置文件
#数据库配置
spring.datasource.druid.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.druid.url=jdbc:mysql:///school
spring.datasource.druid.username=root
spring.datasource.druid.password=123@qwe
#连接虚拟机中的redis
spring.redis.host=192.168.31.35
spring.redis.port=6379
3、创建对应的包
实体类entiey层
@Data
@TableName("stock")
public class Stock {
@TableId(type = IdType.AUTO)
private Integer id;
private Integer num;
}
dao层(继承mybatisplus中的BaseMapper)
public interface StockDao extends BaseMapper<Stock> {
}
controller层
@RestController
@RequestMapping("stock")
public class StockController {
@Autowired
private StockService stockService;
@GetMapping("decsr/{id}")
public String descstock(@PathVariable("id")Integer id){
return stockService.decrStock(id);
}
}
5.1、普通的redis分布锁
加入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
service层
@Service
public class StockService02 {
@Resource
private StockDao stockDao;
@Autowired
private StringRedisTemplate redisTemplate;
public String decrStock(Integer id) {//synchronized () 同步方法 同步代码块
//这种模式缺点:假如线程A进来加锁后,因为某些原因导致时间超时,且还没有释放锁,这时候线程B进来了,导致线程A释放的是线程B的锁...造成线程同步问题
Boolean flag = redisTemplate.opsForValue().setIfAbsent("product::" + id, "fyx",30, TimeUnit.SECONDS);
//查询对应的id的库存
if(flag) {//获取锁了
try {
Stock stock = stockDao.selectById(id);
if (stock.getNum() > 0) {
//根据id修改库存 点击一次数量减一
stock.setNum(stock.getNum() - 1);
//调用修改方法
stockDao.updateById(stock); //异常发生
// int c=10/0;
System.out.println("库存剩余:" + (stock.getNum()));
return "库存减少成功";
} else {
return "库存不足";
}
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}
finally {
redisTemplate.delete("product::" + id);//释放锁资源 一定再finally
}
}else{
System.out.println("服务器正忙请稍后再试..........");
return "服务器正忙请稍后再试..........";
}
}
}
- 结果测试
JMETER压测:
结果:
-
问题
普通的redis可能出现的问题:当redis中的A进程标识到期后,但是当前A进程并没有执行结束,A进程继续执行。然后B进程发现redis中的A进程标识已经消失,B进程就进行加锁,然后B进程也可以进入到了业务执行中,这样又出现了同步安全问题,并且进程A再释放锁,释放就是进程B的锁。
不过,redis给出了解决方法。Redis官方给出了一个高级的协调的Redis客服端–》Redisson。
5.2、使用Redisson实现分布锁
简介:Redisson
Redisson是架设在redis基础上的一个java驻内存数据网络。Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。
原理图:引用自“石杉的架构笔记”
1、添加依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.4</version>
</dependency>
2、启动类
@SpringBootApplication
@MapperScan("com.fyx.dao")
public class FenbushiApplication {
public static void main(String[] args) {
SpringApplication.run(FenbushiApplication.class, args);
}
@Bean //Configuration
public RedissonClient getRedisson(){
Config config=new Config();
config.useSingleServer().setAddress("redis://192.168.31.35:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3、service层
@Service
public class StockService {
@Resource
private StockDao stockDao;
@Resource
private RedissonClient redisson;
public String decrStock(Integer id){
//加锁
synchronized (this){
RLock lock = redisson.getLock("product::" + id);//获取锁对象
try{
lock.lock();
//查询对应的id库存
Stock stock = stockDao.selectById(id);
if (stock.getNum()>0){
//根据id修改库存数量
stock.setNum(stock.getNum()-1);
stockDao.updateById(stock);
System.out.println("库存剩余:"+stock.getNum());
return "库存减少成功";
}else {
return "库存不足";
}
}catch (Exception e){
throw new RuntimeException(e.getMessage());
}finally {
if(lock.isLocked()){ //是否还是锁定状态 ----延迟时间
if(lock.isHeldByCurrentThread()){ // 是当前执行线程的锁
lock.unlock(); // 释放锁
}
}
}
}
}
}
4、测试
Redisson是如何解决问题的
Redisson中有一个“看门狗”模式,就是当线程执行时,会去查看进程有没有执行完,如果还没有执行完,会给进程延长锁的存在时间。看门狗模式中,有一个默认时间30秒,这个时间设置了锁的默认超时时间(lockWatchdogTimeout),然后看门狗会在lockWatchdogTimeout/3,也就是每10秒查看一次当前进程有没有执行完,没有执行完,把时间再延长至30s,等进程执行完毕后,自动释放锁。getLock()创建锁实例时,就会加载默认的时间30s。可以手动配置。lock()方法获取锁,可以在里边设置锁的存活时间,设置之后,就不会再自动释放锁,不会延长锁的存活时间。unlock():释放锁