一、为什么需要分布式锁:
为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。
但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,即synchronized在分布式系统中失效了。 为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
二、什么是分布式锁:
分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
我们先来看下,一把靠谱的分布式锁应该有哪些特征:
三、主流实现分布式锁的技术:
- 基于关系型数据库实现的分布式锁:
利用mysql的乐观锁实现分布式锁(版本号),但是性能完全基于mysql本身,在高并发会直接打垮mysql,用的时候一定要三思!!(由于性能太差,不推荐,也不做后续的对比) - 基于单机redis实现分布式锁:
redis2.6后 SET指令已经支持nx/px/expire,实现加锁非常简单,也是目前99%中小型公司分布式锁的选择。 - 基于redis集群实现的分布式锁:
为了解决单机redis分布式锁的单点故障及可重入性问题,redis推出了红锁。 - 基于zk集群实现的分布式锁:
利用zk有序的临时节点加上它的watch机制实现分布式锁,它的可靠性最高。
对于以上分布式锁实现的对比: 1. 基于关系数据库的分布式锁,一定要在没有大流量的业务且没有其他资源下才会去选择它; 2. 实现复杂度:2,3,4都有对应的客户端,相应的功能已经都封装好,都非常简单; 3. 可靠性(高–低):zk集群 > 红锁 > 单机redis; 4. 性能(高–低):单机redis > 红锁 > zk集群; 5. 成本(高–低):红锁 > zk集群 > 单机redis;
基于以上的对比,也不难发现为什么99%中小型公司的分布式锁要选择 单机redis。
总结:
四、Redis分布式锁实现原理:
加锁过程: redis的加锁就是给Key键设置一个值,且key存在时则不能设置成功,为避免死锁,并给定一个过期时间, 且在redis 2.6 后SET指令已经支持nx/px/expire,它能保证设置key和给key加过期时间的原子性。
具体实现: SET(key,value,NX,PX,time) 其中 key 就是要锁定的资源;value 可以是客户端唯一标识;NX代表只在键不存在时, 才对键进行设置操作;PX 表示对这个key要设置过期时间 ;time 表示key过期的具体时间。
解锁过程: 就是删除掉key来释放资源,但是不能乱删除,需要通过判断key中的value和当前客户端标识一致时才能删除,且整个过程要保证它的原子性。
1.为什么解锁过程要保证原子性呢? 举例说明一下如果解锁过程没有保证原子性的问题,当v1客户端进来拿到锁了,当它准备解锁时在判断客户端标识时拿到value是v1即客户端标识一致, 此时刚好它加锁的key过期了,同时v2客户端加锁成功了,则v1会删除属于v2的key,这种情况下就属于误删,则解锁的过程必须要保证原子性。
2.那么如何保证加锁过程保证原子性? 使用lua脚本,在lua脚本中的指令可以保证它的原子性执行。(lua脚本也不支持回滚,但可以手动回滚)
解锁的lua脚本如下:
-------------------------------------------------------
if redis.call('get',KEYS[1]) == ARGV[1] then
return redis.call('del',KEYS[1])
else
return 0
end
-------------------------------------------------------
五、Redis分布式锁的演化升级:
redis分布式锁的实现原理图:
redis分布式锁的使用场景:
多个服务间+保证同一时刻+同一个用户只能有一个请求(防止关键业务出现数据冲突和并发错误)
代码演示redis分布式锁: 创建一个空项目redis:
1.在该项目下创建两个模块:
boot_redis01 boot_redis02:boot_redis02是完全复制的模块一
2.改pom:
common-pool2的对象池依赖: 我们在服务器开发的过程中,往往会有一些对象,它的创建和初始化需要的时间比较长,比如数据库连接,网络IO,大数据对象等。在大量使用这些对象时,如果不采用一些技术优化,就会造成一些不可忽略的性能影响。一种办法就是使用对象池,每次创建的对象并不实际销毁,而是缓存在对象池中,下次使用的时候,不用再重新创建,直接从对象池的缓存中取即可。为了避免重新造轮子,我们可以使用优秀的开源对象池化组件apache-common-pool2,它对对象池化操作进行了很好的封装,我们只需要根据自己的业务需求重写或实现部分接口即可,使用它可以快速的创建一个方便,简单,强大对象连接池管理类。
使用common-pool2的对象池技术的一个完美例子就是redis的Java客户端JedisPool。
综上所述,使用common-pool2可以快速的创建一个安全,强大,简单的对象池管理类。它的开源性使它的功能得到了众多项目的检测,是非常安全的。在我们的业务中,如果有需要使用对象池化的操作,可以使用common-pool2快速实现。
jedis依赖: Jedis是一个jar包,是Redis官方推荐的用于java访问redis的客户端,主要是用来帮助连接使用数据库。Java项目使用Redis数据库,新版的默认是lettuce客户端,如果要使用jedis客户端,需要先排除lettuce,然后添加jedis客户端。 如果是Maven项目,在pom.xml文件中添加下面两种支持:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
引入redisson依赖 由于我们是springboot整合redisson,所以我们只需引入springboot-redisson-starter就可以了, 不过这里需要注意springboot与redisson的版本,因为官方推荐redisson版本与springboot版本配合使用。
将 Redisson 与 Spring Boot 库集成。取决于Spring Data Redis模块,支持 Spring Boot 1.3.x - 2.4.x
这句话是官方说的,不过现在的2.5.x也是支持的,只需要注意springboot最低版本不要低于1.3.x即可。
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.6</version>
</dependency>
总的pom:
<?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 https://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.6.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.fan</groupId>
<artifactId>boot_redis01</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot_redis01</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--redis相关的三个依赖redis+jedis+commons-pool2 -->
<!--spring boot data redis 依赖 (需要配合 commons-pool2 对象池依赖使用)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jedis:java访问redis的客户端,这个单独自己增加-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
<!--commons-pool2 ,如果使用Lettuce作为连接池,
需要引入commons-pool2包,
否则会报错bean注入失败-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--web+actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--spring-boot-starter-actuator
自动配置模块用于支持 SpringBoot 应用的监控-->
<!--spring-boot-starter-actuator
可以用于检测系统的健康情况、当前的Beans、系统的缓存等-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--redisson-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.写yml:
这里使用Lettuce 客户端的连接池:
server.port=1111
#redis
spring.redis.host=192.168.211.210
spring.redis.dataabase=0
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制) 默认 8
spring.redis.lettuce.pool.max-active=500
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接 默认 8
spring.redis.lettuce.pool.max-idle=8
# 连接池中的最小空闲连接 默认 0
spring.redis.lettuce.pool.min-idle=0
## 连接超时时间(毫秒)
#spring.redis.timeout=30000
4.主启动类:自动生成的
5.配置类conf.RedisConfig:
6.controller:
package com.fan.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
public class GoodsController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy")
public String buy_Goods(){
System.out.println("test");
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNum = result == null ? 0 : Integer.parseInt(result);
if(goodsNum > 0){
int realNum = goodsNum - 1;//卖出一个商品
stringRedisTemplate.opsForValue().set("goods:001",String.valueOf(realNum));
System.out.println("成功买到商品,库存还剩下:"
+realNum+"件,\t 服务提供的端口:"+ serverPort);
return "成功买到商品,库存还剩下:"
+realNum+"件,\t 服务提供的端口:"+ serverPort;
}else{
System.out.println("商品卖完了!!!\t 服务提供的端口:"+ serverPort);
}
return "商品卖完了!!!\t 服务提供的端口:"+ serverPort;
}
@GetMapping("/redis")
public String test01(){
stringRedisTemplate.opsForValue().set("name","tom");
String name = (String)stringRedisTemplate.opsForValue().get("name");
return name;
}
}
同样,创建第二个微服务,使用一个空的工程,包含一个父工程,父工程下包含两个微服务模块;
|