1. 分布式锁
1.1 什么是分布式锁
在我们进行单机应用开发涉及并发同步的时候,我们往往采用synchronized或者ReentrantLock的方式来解决多线程间的代码同步问题。但是当我们的应用是在分布式集群工作的情况下,那么就需要一种更加高级的锁机制,来处理种跨机器的进程之间的数据同步问题,这就是分布式锁。
分布式锁,是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。
分布式锁可以理解为:控制分布式系统有序的去对共享资源进行操作,通过互斥来保证数据的一致性。
可能有同学会问,使用我们以前学习的Java中的锁机制,例如synchronized、ReentrantLock不就能解决问题了吗?为什么还要使用分布式锁?
对于简单的单体项目,即运行时程序在同一个Java虚拟机中,此时使用上面的Java的锁机制确实可以解决多线程并发问题。例如下面程序代码:
public class LockTest implements Runnable { public synchronized void get() { System.out.println("1 线程 -->" + Thread.currentThread().getName()); System.out.println("2 线程 -->" + Thread.currentThread().getName()); System.out.println("3 线程 -->" + Thread.currentThread().getName()); } public void run() { get(); } public static void main(String[] args) { LockTest test = new LockTest(); for (int i = 0; i < 10; i++) { new Thread(test, "线程-" + i).start(); } } }
运行结果如下:
1 线程 -->线程-0 2 线程 -->线程-0 3 线程 -->线程-0 1 线程 -->线程-2 2 线程 -->线程-2 3 线程 -->线程-2 1 线程 -->线程-1 2 线程 -->线程-1 3 线程 -->线程-1 1 线程 -->线程-3 2 线程 -->线程-3 3 线程 -->线程-3 1 线程 -->线程-4 2 线程 -->线程-4 3 线程 -->线程-4但是在分布式环境中,程序是集群方式部署,如下图:
上面的集群部署方式依然会产生线程并发问题,因为synchronized、ReentrantLock只是jvm级别的加锁,没有办法控制其他jvm。也就是上面两个tomcat实例还是可以出现并发执行的情况。要解决分布式环境下的并发问题,则必须使用分布式锁。
分布式锁的实现方式有多种,例如:数据库实现方式、ZooKeeper实现方式、Redis实现方式等。
1.2 为什么要使用分布式锁
为了能够说明分布式锁的重要性,下面通过一个电商项目中减库存的案例来演示如果没有使用分布式锁会出现什么问题。代码如下:
第一步:导入坐标
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> </parent> <groupId>com.itheima</groupId> <artifactId>lock-test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--集成redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>1.4.1.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> </project>
第二步:配置application.yml文件
server: port: 8080 spring: redis: host: 68.79.63.42 port: 26379 password: itheima123
第三步:编写Controller
package com.itheima.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class StockController { @Autowired private StringRedisTemplate redisTemplate; @GetMapping("/stock") public String stock(){ int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("库存扣减成功,剩余库存:" + stock); }else { System.out.println("库存不足!!!"); } return "OK"; } }
第四步:编写启动类
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class,args); } }
第五步:设置redis
测试方式:使用jmeter进行压力测试,如下:
注:Apache JMeter是Apache组织开发的基于Java的压力测试工具。用于对软件做压力测试,它最初被设计用于Web应用测试,但后来扩展到其他测试领域。
查看控制台输出,发现已经出现了线程并发问题,如下:
由于当前程序是部署在一个Tomcat中的,即程序运行在一个jvm中,此时可以对减库存的代码进行同步处理,如下:
再次进行测试(注意恢复redis中的数据),此时已经没有线程并发问题,控制台输出如下:
这说明如果程序运行在一个jvm中,使用synchronized即可解决线程并发问题。
下面将程序进行集群部署(如下图所示),并通过Nginx进行负载,再进行测试。
操作过程:
第一步:配置Nginx
upstream upstream_name{ server 127.0.0.1:8001; server 127.0.0.1:8002; } server { listen 8080; server_name localhost; location / { proxy_pass http://upstream_name; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
第二步:修改application.yml中端口号改为8001和8002并分别启动程序
第三步:使用jemter再次测试,可以看到又出现了并发问题
1.3 分布式锁应具有的特性
- 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
- 高可用的获取锁与释放锁
- 高性能的获取锁与释放锁
- 具备可重入特性
- 具备锁失效机制,防止死锁
- 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败