Redis 分布式锁(一)
前言
本文力争以最简单的语言,以博主自己对分布式锁的理解,按照自己的语言来描述分布式锁的概念、作用、原理、实现。如有错误,还请各位大佬海涵,恳请指正。分布式锁分两篇来讲解,本篇讲解客户端,下一篇讲解redis服务端。
概念
如果把分布式锁的概念搬到这里,博主也会觉得枯燥。博主这里以举例的形式来描绘它。
试想一种场景,在一个偏远小镇上的火车站,只有一个售票窗口。
火车站来了10名旅客,前往售票窗口购买火车票,旅客只能排队购票,排到第一的旅客,可以与售票员沟通,买票。
好啦,以上就是一个分布式锁的场景,我们来分析一下每一个细节。
每位旅客可以理解为一个系统或者线程。他们在竞争售票员的工作时间。
是不是觉得分布式锁也不是什么高大上的概念。有同学会问,锁到底在哪里呢?还是买票场景,我们看看锁长什么样子。
我们深入想一下,这10位旅客本来是并行的(没有买票前,他们有的在吃饭,有的在玩手机,等等等),而到了买票的时候,就必须排队(串行),而不是一起买票。
没错,就是在特定的场景下,将并行的场景,变成穿行,就是分布式锁的奥义所在。
作用
分布式锁的作用不但非常大,而且非常多。
在软件设计中,比如电商秒杀活动。商家预备了1000件货物,也就只有这1000件货,有1500人参与秒杀,可以理解为1500个线程来排队购买商品。那就必须将这1500个线程排个队(比如按照时间),设置一把锁,一个购买过程结束,再开始下一个。
为什么redis可以实现分布式锁呢?
我们以购票举例,购票窗口前的这个锁,是每位旅客都可以看到的。
这里我们可以得出一个结论,一把锁首先要具有的属性是:想要获得锁的人都可以看到。
这把锁既不能属于服务器A,也不能属于服务器B,因为他们都不知道另一方的存在,那就必须选择一个公信的第三方来作为锁。当当当~ redis闪亮登场。当然zookeeper也可以实现,这里先挖一个坑,以后再填zookeeper吧。
原理
加锁的基本思路
redis中有一条指令非常有意思,它叫做setnx
当redis中不存在key值为“lock”的时候,可以设置成功;当存在key值时,设置失败。
这句指令,好比是,询问一下,到我买票了吗?返回结果是1的时候,到您买票了;返回结果是0的时候,还没到您,稍后再询问。
我们的锁过程可以这样来操作:
- setnx lock 锁值
- 处理业务逻辑
- 释放锁 del lock
优化一
为什么要优化?
试想,如果setnx lock 1 加锁成功,这个时候系统因为其他原因,挂掉了,就永远无法执行del lock了。
要避免这种情况,怎么办呢?给锁一个过期时间。
这样无论系统是否宕机,都会在10秒后释放锁。看似很美好,虽然setnx lock 1 与 expire lock 10之间的时间间隙非常小,但仍然有风险,加入系统执行完 setnx lock 1 后,宕机了,并没有执行 过期指令 expire lock 10,再次产生了一把无法解开的锁,“死锁”。
这时候引入了一个概念,叫做原子操作。即这两条指令需要在一个原子操作内执行完成。
set key value [expiration EX seconds|PX milliseconds] [NX|XX]
优化二
why?上一个优化已经把上锁过程做成了原子操作,还需要什么优化呢?
当然有,试想一下,之前代码set lock 1 ex 10 nx,设置过期时间是10秒,那么这个10秒是否可靠呢?显然不可靠。
我们加锁的过程是 加锁---执行业务代码---释放锁
加入业务代码的执行时间超过10秒呢?是不是业务代码还没有执行完,锁就已经释放了。放在购票场景中,第一位旅客还没有完成购票,第二位旅客就开始购票。显然不合理。怎么办呢?
这里我们需要估计业务代码的执行时间,加入预估出来的时间是10秒,可以在业务代码中开辟一个“续命”的操作。
- 加锁 set lock 1 ex 10 nx
- 每过3秒,把该锁的时间重新设置为 10秒
- 执行业务代码
- 释放锁 del lock
这里的续命时间间隔 = 过期时间 10S / 3
这样设置比较合理,可以防止一次续命失败。
优化三
纳尼?还有问题吗?
有,而且可以算是一个bug,我们一直在用 set lock 1 ex 10 nx 来加锁,用del lock 来释放锁。
我们需要明确知道,释放的锁,是自己加上的。
可以set lock uuid ex 10 nx 来解决该问题。
拓展-可重入锁
一个线程获取到锁以后,再次获取锁,就是可重入锁。
但博主现在遇到的问题,一般不需要可重入锁即可解决。java中ReentrantLock就是可重入锁。
可重入锁,对代码的复杂度增加了很多,玩不好,容易扯裆。谨慎使用。
实现
已经讲了很多优化相关的内容,这里博主就直接写优化后的代码了。
博主使用java来实现。而redis官方(https://redis.io/clients#java)推荐的有三个框架。分别是Jedis、lettuce、Redisson。
由于博主在本篇中主要讨论单个redis的情况,而redisson主要用来处理分布式redis,下一篇博文使用redisson,敬请期待。
springboot2.x 默认采用了 lettuce,所以博主就使用lettuce来实现分布式锁。
引入依赖
<!-- data-redis中集成了lettuce -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- redis链接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- alibaba json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
配置文件
既然要测试分布式锁,那么就至少应该跑两份代码,所以配置文件也应该是两份,这里博主偷个懒,提供一份配置文件,另一份配置文件修改下server的端口即可。
server:
port: 80
spring:
redis:
# redis的ip地址
host: redis的ip地址
# redis的端口号
port: 6379
# redis的密码
password: 你的密码
lettuce:
pool:
# 最大链接数
max-active: 30
# 链接池中最大空闲链接数
max-idle: 15
# 最大阻塞等待链接时长 默认不限制 -1
max-wait: 2000
# 最小空闲链接数
min-idle: 10
# 链接超时时长
shutdown-timeout: 10000
lettuce配置类
这个类博主就不细讲了,springboot整合lettuce,序列化博主更偏爱FastJson
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author xujp
* redis 配置类 将RedisTemplate交给spring托管
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(genericFastJsonRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(genericFastJsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
分布式锁
重头戏来了,手写分布式锁的核心代码示例。
import com.redis.demo1.thread.WatchDog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @author xujp
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private RedisTemplate redisTemplate;
@GetMapping
public void lock(){
String uuid = UUID.randomUUID().toString();
//System.out.println(uuid);
WatchDog watchDog;
try {
// 自旋
while (true) {
// 尝试获取锁
Boolean hasLock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3l, TimeUnit.SECONDS);
if(hasLock) {
// 看门狗“续命“
watchDog = new WatchDog(redisTemplate, uuid);
watchDog.start();
// 业务逻辑start
int num = (int) redisTemplate.opsForValue().get("num");
//Thread.sleep(4000); // 假设业务需要4s处理时间
redisTemplate.opsForValue().set("num", num - 1);
System.out.println(num);
// 业务逻辑处理 end
break;
}else{
// 睡眠100ms再自旋
Thread.sleep(100);
}
}
}catch (Exception e){
System.out.println(e);
}finally {
// 关闭锁
String l = (String) redisTemplate.opsForValue().get("lock");
if (l.equalsIgnoreCase(uuid)) {
redisTemplate.delete("lock");
}
}
}
}
分布式锁“续命”代码示例
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author xujp
*/
public class WatchDog extends Thread {
private RedisTemplate redisTemplate;
private String uuid;
public WatchDog(RedisTemplate redisTemplate, String uuid){
this.redisTemplate = redisTemplate;
this.uuid = uuid;
}
public void run(){
// 续命逻辑
while (true){
try {
// 获取锁的value
Object redisUUID = redisTemplate.opsForValue().get("lock");
// 判断当前父线程是否已经释放锁,如果父线程已释放,则跳出线程
if(redisUUID==null || !redisUUID.toString().equals(uuid)){
break;
}
// 续命
redisTemplate.expire("lock", 3l, TimeUnit.SECONDS);
// 没隔1s续命一次
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
}
测试
首先我们将代码分别以80和81端口run起来。
有精力的同学,还可以再搭建一个nginx将请求分流到80和81。这里博主简单粗暴地使用jmeter请求。
博主使用jmeter来测试,博主默认大家都会使用(不会使用的童鞋需要学习喽)。
jmeter准备工作
在jmeter中设置50个线程
在该线程下设置两个接口,分别请求80和81
redis准备工作
在redis中设置一对键值 num
至此,就可以在jmeter中开启请求了
测试结果
我们先来看redis中num的值
我们再分别查看80和81的日志
总结
本文讲述了利用redis实现分布式锁的原理,分布式锁本质上是将并发请求按顺序处理,那么这把锁就成为了所有请求的瓶颈,如何打破锁的瓶颈呢?敬请关注博主,后续填坑(博主挖坑必填)。
本文留下的两个坑:
1,为了使redis高可用,redis集群后,如何解决redis端因为网络问题导致锁不同步问题?
2,分布式锁实现了并发排队,锁成为了性能瓶颈,如何提高性能?
Redis 分布式锁(一)的更多相关文章
- 利用redis分布式锁的功能来实现定时器的分布式
文章来源于我的 iteye blog http://ak478288.iteye.com/blog/1898190 以前为部门内部开发过一个定时器程序,这个定时器很简单,就是配置quartz,来实现定 ...
- Redis分布式锁
Redis分布式锁 分布式锁是许多环境中非常有用的原语,其中不同的进程必须以相互排斥的方式与共享资源一起运行. 有许多图书馆和博客文章描述了如何使用Redis实现DLM(分布式锁管理器),但是每个库都 ...
- redis分布式锁和消息队列
最近博主在看redis的时候发现了两种redis使用方式,与之前redis作为缓存不同,利用的是redis可设置key的有效时间和redis的BRPOP命令. 分布式锁 由于目前一些编程语言,如PHP ...
- redis咋么实现分布式锁,redis分布式锁的实现方式,redis做分布式锁 积极正义的少年
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- spring boot redis分布式锁
随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁.分布式锁的实现有很多种,比如基于数据库. zookeeper 等,本文主要介绍使用 Redis 做分布式锁的方式,并封装成spring b ...
- Redis分布式锁的正确实现方式
前言 分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介 ...
- Redis分布式锁---完美实现
这几天在做项目缓存时候,因为是分布式的所以需要加锁,就用到了Redis锁,正好从网上发现两篇非常棒的文章,来和大家分享一下. 第一篇是简单完美的实现,第二篇是用到的Redisson. Redis分布式 ...
- redis分布式锁实践
分布式锁在多实例部署,分布式系统中经常会使用到,这是因为基于jvm的锁无法满足多实例中锁的需求,本篇将讲下redis如何通过Lua脚本实现分布式锁,不同于网上的redission,完全是手动实现的 我 ...
- Redis分布式锁的try-with-resources实现
Redis分布式锁的try-with-resources实现 一.简介 在当今这个时代,单体应用(standalone)已经很少了,java提供的synchronized已经不能满足需求,大家自然 而 ...
- 关于分布式锁原理的一些学习与思考-redis分布式锁,zookeeper分布式锁
首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...
随机推荐
- 特性速览| Apache Hudi 0.5.3版本正式发布
1. 下载连接 源代码下载:Apache Hudi 0.5.3 Source Release (asc, sha512) 0.5.3版本相关jar包地址:https://repository.apac ...
- 使用addEventListener绑定事件是关于this和event记录
DOM元素使用addEventListener绑定事件的时候经常会碰到想把当前作用域传到函数内部,可以使用以下两种放下: var bindAsEventListener=function (objec ...
- [ C++ ] 勿在浮沙筑高台 —— 内存管理(18~31p) std::alloc
部分内容个人感觉不是特别重要,所以没有记录了.其实还是懒 embedded pointers 把对象的前四字节当指针用. struct obj{ struct obj *free_list_link; ...
- 【面试篇】寒冬求职之你必须要懂的Web安全
https://segmentfault.com/a/1190000019158228 随着互联网的发展,各种Web应用变得越来越复杂,满足了用户的各种需求的同时,各种网络安全问题也接踵而至.作为前端 ...
- -手写Spring注解版本&事务传播行为
视频参考C:\Users\Administrator\Desktop\蚂蚁3期\[www.zxit8.com] 0018-(每特教育&每特学院&蚂蚁课堂)-3期-源码分析-手写Spri ...
- 如何修改linux下tomcat指定的jdk路径
一般情况下,一台服务器只跑一个项目,只需根据所需项目,将linux默认的jdk环境配置好即可.某些时候一台服务器上会跑多个项目,而且各个项目需要的JDK版本各不相同,或者为了使业务独立开来,需要指定T ...
- 【SpringMVC】
前言
- Angular 从入坑到挖坑 - 模块简介
一.Overview Angular 入坑记录的笔记第七篇,介绍 Angular 中的模块的相关概念,了解相关的使用场景,以及知晓如何通过特性模块来组织我们的 Angular 应用 对应官方文档地址: ...
- Python实用笔记 (6)函数
绝对值 >>> abs(100) 100 >>> abs(-20) 20 max()可以接收任意多个参数,并返回最大的那个: >>> max(1, ...
- jquery入门(3)
4.jQuery中的事件绑定 4.1.事件绑定 on方法绑定 $('#box').on('click',function(){ alert(1); }) 直接绑定 $("#box" ...