【问题描述】synchronized只能解决一个tomcat的并发问题,synchronized锁的一个进程下的线程并发,如果分布式环境,多个进程并发,这种方案就失效了!
【自己实现分布式锁的思路】
1. 因为redis是单线程的,所以命令也就具备原子性,使用setnx命令实现锁,保存k-v
如果k不存在,保存(当前线程加锁),执行完成后,删除k表示释放锁
如果k已存在,阻塞线程执行,表示有锁
2. 如果加锁成功,在执行业务代码的过程中出现异常,导致没有删除k(释放锁失败),那么就会造成死锁(后面的所有线程都无法执行)!
设置过期时间,例如10秒后,redis自动删除
3. 高并发下,由于时间段等因素导致服务器压力过大或过小,每个线程执行的时间不同
第一个线程,执行需要13秒,执行到第10秒时,redis自动过期了k(释放锁)
第二个线程,执行需要7秒,加锁,执行第3秒(锁 被释放了,为什么,是被第一个线程的finally主动deleteKey释放掉了)
。。。连锁反应,当前线程刚加的锁,就被其他线程释放掉了,周而复始,导致锁会永久失效
4. 给每个线程加上唯一的标识UUID随机生成,释放的时候判断是否是当前的标识即可
5. 问题又来了,过期时间如果设定?
如果10秒太短不够用怎么办?
设置60秒,太长又浪费时间
可以开启一个定时器线程,当过期时间小于总过期时间的1/3时,增长总过期时间(吃仙丹续命!)
总之一句话,自己实现分布式锁比较难,我们可以借助Redssion
下面通过Redis实现分布式锁,解决高并发问题
【Redisson介绍】
- Redis是最流行的 NoSQL 数据库解决方案之一,而 Java 是世界上最流行的编程语言之一。
- 虽然两者看起来很自然地在一起“工作”,但是要知道,Redis 其实并没有对 Java 提供原生支持。
- 相反,作为 Java 开发人员,我们若想在程序中集成 Redis,必须使用 Redis 的第三方库。
- 而Redisson 就是用于在 Java 程序中操作 Redis 的库,它使得我们可以在程序中轻松地使用Redis。
- Redisson 在 java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类。
【模拟秒杀项目结构】:
pom.xml文件:
<?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>
<groupId>com.DJL</groupId>
<artifactId>killers</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!--实现分布式锁的工具类-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.1</version>
</dependency>
<!--spring操作redis的工具类-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!--redis客户端-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
<!--json解析工具-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8002</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
spring.xml文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="controller"/>
<bean id="stringRedisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
<property name="connectionFactory" ref="connectionFactory"></property>
</bean>
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="192.168.239.131"></property>
<property name="port" value="6379"/>
</bean>
</beans>
controller包下的TestKill文件(模拟秒杀的业务,同时使用了分布式锁):
package controller;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.TimeUnit;
/**
* @author DJL
*/
@RestController
public class TestKill {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Autowired
Redisson redisson;
@RequestMapping("/kill")
public synchronized String kill() {
//定义商品id
String productKey = "HUAWEI-P40";
//通过redssion获取锁
RLock rlock = redisson.getLock(productKey);//底层源码就是集成了setnx、过期时间等操作
//上锁(过期时间为30秒)
rlock.lock(30, TimeUnit.SECONDS);
try {
//1.从redis中获取手机的库存量
int phoneCount = Integer.parseInt(stringRedisTemplate.opsForValue().get("phone"));
if (phoneCount > 0) {
phoneCount--;
stringRedisTemplate.opsForValue().set("phone", phoneCount + "");
System.out.println("库存-1,剩余:" + phoneCount);
} else {
System.out.println("库存不足");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
rlock.unlock();
}
return "over";
}
@Bean
public Redisson redisson() {
Config config = new Config(); // 使用单个redis服务器
config.useSingleServer().setAddress("redis://192.168.239.131:6379").setDatabase(0); // 使用集群redis
// config.useClusterServers().setScanInterval(2000).addNodeAddress("redis://192.168 .204.141:6379","redis://192.168.204.142:6379","redis://192.168.204.143:6379");
return (Redisson) Redisson.create(config);
}
}
我们启动两个工程,一个tomcat端口8001,一个tomcat端口8002,同时使用nginx进行负载均衡,同时我开启了两个虚拟机,一个虚拟机上使用Redis存储数据,另外一个虚拟机进行负载均衡
(1)对虚拟机上Nginx的Nginx.conf配置文件做出如下修改:
upstream sga{
server 192.168.204.1:8001; //192.168.204.1是我本机的ip地址,8001是tomcat的端口号
server 192.168.204.1:8002; //8002是另外一个工程的tomcat端口号
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://sga;
root html;
index index.html index.htm;
}