【Redis 分布式锁】(1)一把简单的“锁”
原文链接:https://www.changxuan.top/?p=1230
在单体架构向分布式集群架构演进的过程中,项目中必不可少的一个功能组件就是分布式锁。在开发团队有技术积累的情况下,做为团队的一个“工具人”往往有限的时间都投入到了业(C)务(U)开(R)发(D)上,并不会去深究工具类中的分布式锁到底是如何实现的。大家只需要清楚如何使用某个同事写好的 Redis 工具类就可以了。所以,今天就带大家从零开始实现一个基于Redis的可以在项目中直接使用的分布式锁。
首先,需要搞清楚一个问题,我们为什么需要分布式锁或者说为什么需要锁?下面我们通过一张图来说明这个问题,

在上面这张图中,同时有两个线程 Thread A 和 Thread B 要做同一件事,你可以理解它们为要同时执行一个代码块。但是,执行这个代码块有个特殊的要求:不能有多个线程同时执行,不然系统数据就会出乱子!所有需要执行这段代码的线程,需要挨个排队来执行。所以,此时就需要有一个所有线程都能访问到的一个变量,根据这个变量的状态来判断此时是否有其它线程正在执行,来决定当前线程是否能够执行,那么这个变量就是“锁”。假如设变量为 static volitate int lock = 0; ,当 lock 的值为 0 时,表明此时没有线程在执行这段有特殊要求的代码,当 lock 的值为 1 时,表明此时有其它线程在执行这段有特殊要求的代码。当某个线程获取到 lock 值为 0,且将 lock 值改为 1 的过程,称为成功获取锁(注:此过程需要是原子性的);当该线程执行完这段代码后,将 lock 的值改为 0 的操作称为释放锁。
在分布式系统中,由于子系统需要支持水平扩展所以就不能把内存变量的状态做为“一把锁”了。不过我们可以把变量放到 Redis 中,这样所有节点的线程都能够访问和操作了。
一、 一把简单的“锁”
“一口吃不成个大胖子”,我们先实现一个最简单 Redis 锁。我们通过逐渐发现问题并解决的过程,来加深理解。
根据前文所描述的锁的基本原理,我首先写了两个方法,一个是获取锁 boolean lock(String key) ,一个是释放锁 void unlock(String key)。
@RedisUtil.java
/**
* 加锁
* @param key 锁名称
* @return lock 是否获取锁, true:获取, false:未获取
*/
public boolean lock(String key) {
boolean lock;
try {
// setIfAbsent 等价于 Redis 的 setnx 命令,具有原子性
lock = Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, "LOCK"));
} catch (Exception e) {
log.error("获取锁:{},出现异常{}", key, e);
return false;
}
return lock;
}
/**
* 释放锁
* @param key 锁名称
*/
public void unlock(String key) {
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
redisTemplate.delete(key);
}
}
下面我们就写代码来测试一下,
测试思路:
创建十个线程调用 work() 方法
加锁:方法获取到锁的线程对变量 count 执行1000次自增操作,未获取到锁的线程则不执行自增操作。
不加锁:由于自增操作是非原子性的,所以最终 count 的结果会小于 10000 大于 1000 。
@WorkService.java
public static int count = 0;
private static final int TIME = 1000;
public void work() {
String key = "TEST_KEY";
if (redisUtil.lock(key)) {
log.info("线程:{},已经获取锁", Thread.currentThread().getName());
try {
for (int i = 0; i < TIME; i++) {
WorkService.count++;
}
}catch (Exception e) {
log.error("发生错误",e);
}finally {
redisUtil.unlock(key);
log.info("线程:{},已经释放锁", Thread.currentThread().getName());
}
} else {
log.info("线程:{},未获取到锁", Thread.currentThread().getName());
}
}
public void notLockWork() {
for (int i = 0; i < TIME; i++) {
WorkService.count++;
}
}
@WorkServiceTest.java
@Test
void work() throws Exception {
CountDownLatch downLatch = new CountDownLatch(10);
LinkedList<Thread> threads = new LinkedList<>();
for (int i = 0; i < 10; ++i) {
Thread thread = new Thread(() -> {
// 加锁测试
workService.work();
// 未加锁测试
// workService.notLockWork()
downLatch.countDown();
});
threads.add(thread);
}
for (Thread thread : threads) {
thread.start();
}
downLatch.await();
System.out.println("count = " + WorkService.count);
}
加锁测试结果(控制台输出)
2021-01-17 16:52:35.256 INFO 8648 --- [Thread-150] com.cxcoder.services.WorkService : 线程:Thread-150,已经获取锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-152] com.cxcoder.services.WorkService : 线程:Thread-152,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-149] com.cxcoder.services.WorkService : 线程:Thread-149,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-154] com.cxcoder.services.WorkService : 线程:Thread-154,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-148] com.cxcoder.services.WorkService : 线程:Thread-148,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-153] com.cxcoder.services.WorkService : 线程:Thread-153,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-151] com.cxcoder.services.WorkService : 线程:Thread-151,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-146] com.cxcoder.services.WorkService : 线程:Thread-146,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-147] com.cxcoder.services.WorkService : 线程:Thread-147,未获取到锁
2021-01-17 16:52:35.277 INFO 8648 --- [Thread-155] com.cxcoder.services.WorkService : 线程:Thread-155,未获取到锁
2021-01-17 16:52:35.320 INFO 8648 --- [Thread-150] com.cxcoder.services.WorkService : 线程:Thread-150,已经释放锁
count = 1000
未加锁测试结果(控制台输出)
count = 3287
从控制台输出结果来看,目前的锁已经可以用来限制某块代码在某一时刻只能有一个线程在执行。但是也能够发现,该锁的实现机制还存在一些问题和不能满足的需求。
例如,如果在项目里中出现”恶意代码“或者不规范代码的情况下则会出现预料之外的结果。看下面的例子,
@WorkService.java
public void work(){
String key = "TEST_KEY";
try {
if (redisUtil.lock(key)){
log.info("线程:{},获取锁", Thread.currentThread().getName());
... ... //(A)
}
}catch (Exception e) {
log.error("发生错误",e);
}finally {
redisUtil.unlock(key);
log.info("线程:{},释放锁", Thread.currentThread().getName());
}
}
如果 void work() 是上面的这种写法,会出现什么问题呢?当有线程X获取到锁后,正在执行 A 处的代码时。这时 线程B 来到后获取锁失败,却执行了 finally 里的代码将锁给释放了。此时 线程A 还在执行的过程中,又来了 线程C 获取到锁后也开始执行。所以,这个 Redis 锁的实现机制存在一个比较严重的问题是某个线程所持有的锁可以被其它线程随意给释放掉。另外,从控制台输出的结果中可以看出在某个线程持有锁的时间段内,其它线程是未被阻塞的。目前这个锁应该被称为存在问题的基于Redis的非阻塞的分布式锁。
【Redis 分布式锁】(1)一把简单的“锁”的更多相关文章
- Redis分布式锁实现Redisson 15问
大家好,我是三友. 在一个分布式系统中,由于涉及到多个实例同时对同一个资源加锁的问题,像传统的synchronized.ReentrantLock等单进程情况加锁的api就不再适用,需要使用分布式锁来 ...
- MyBatisPlus乐观锁,乐观锁竟然如此简单
乐观锁 在便是过程中,我们经常会被问到乐观锁,悲观锁,都非常简单 乐观锁:顾名思义,思想十分乐观,总是认为不会出现问题,无论什么都不去上锁!如果出现了问题,就再更新测试 悲观锁:顾明思义,思想十分悲观 ...
- 卧槽,redis分布式如果用不好,坑真多
前言 在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中. 但不是说用了redis分布式锁,就可以高枕无忧了,如果没有用好或者用对,也会引 ...
- Redis分布式锁实现简单秒杀功能
这版秒杀只是解决瞬间访问过高服务器压力过大,请求速度变慢,大大消耗服务器性能的问题. 主要就是在高并发秒杀的场景下,很多人访问时并没有拿到锁,所以直接跳过了.这样就处理了多线程并发问题的同时也保证了服 ...
- 单实例redis分布式锁的简单实现
redis分布式锁的基本功能包括, 同一刻只能有一个人占有锁, 当锁被其他人占用时, 获取者可以等待他人释放锁, 此外锁本身必须能超时自动释放. 直接上java代码, 如下: package com. ...
- 自己写了个简单的redis分布式锁【我】
自己写了个简单的redis分布式锁 [注意:此锁需要在每次使用前都创建对象,也就是要在线程内每次都创建对象后使用] package redis; import java.util.Collection ...
- redis 分布式锁的简单使用
RedisLock--让 Redis 分布式锁变得简单 目录 1. 项目介绍 2. 快速使用 2.1 引入 maven 坐标 2.2 注册 RedisLock 2.3 使用 3. 参与贡献 4. 联系 ...
- Lua脚本在redis分布式锁场景的运用
目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...
- 4、redis 分布式锁
1. 前言 关于分布式锁的实现,目前常用的方案有以下三类: 数据库乐观锁: 基于分布式缓存实现的锁服务,典型代表有 Redis 和基于 Redis 的 RedLock: 基于分布式一致性算法实现的锁服 ...
随机推荐
- C语言实现汉诺塔
汉诺塔 要把A柱子上的盘子移动到C柱子上,在移动过程中可以借助B柱子,但是要求小的盘子在上大的盘子在下. 解题思路: 1.把A柱子上的前N-1个盘子借助C柱子,全部移动到B柱子上(过程暂不考虑),再把 ...
- 前台生成JSON
方法一 :在后台需要转换String - json let param = new URLSearchParams(); param.append('username', this.username) ...
- ESXi 中重新启动管理代理
使用直接控制台用户界面 (DCUI)重启管理代理: 连接到您的 ESXi 主机的控制台. 按 F2 自定义系统. 以 root 身份登录. 使用上下箭头导航至故障排除选项>重新启动管理代理. 按 ...
- java的多线程:java安全问题产生的原因与JMM的关系
一.多线程产生安全问题 1.Java内存模型 共享内存模型指的就是Java内存模型(简称JMM),JMM决定一个线程对共享变量的写入时,能对另一个线程可见. 从抽象的角度来看,JMM定义了线程和主内存 ...
- linuix查端口
根据进程pid查端口:netstat -nap | grep pid 根据端口port查进程:netstat -nap | grep port 根据pid查找文件的启动位置 ps aux | gre ...
- [每日一题]面试官问:谈谈你对ES6的proxy的理解?
[每日一题]面试官问:谈谈你对ES6的proxy的理解? 关注「松宝写代码」,精选好文,每日一题 作者:saucxs | songEagle 一.前言 2020.12.23 日刚立的 flag,每日一 ...
- WPF ToolTip 绑定
Tool绑定: 绑定到ComboBox的SelectValue <ComboBox Name="cmb_WHSCD" VerticalAlignment="Cent ...
- 【Flutter】容器类组件之填充
前言 Padding可以给其子节点添加填充(留白). 接口描述 class EdgeInsets extends EdgeInsetsGeometry { // 分别指定四个方向的填充 const E ...
- 剑指offer 面试题3:数组中重复的数字
题目描述 在一个长度为n的数组里的所有数字都在0到n-1的范围内. 数组中某些数字是重复的,但不知道有几个数字是重复的.也不知道每个数字重复几次.请找出数组中任意一个重复的数字. 例如,如果输入长度为 ...
- Session、Cookie与Token
http协议是无状态协议 协议是指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则,超文本传输协议(HTTP)是一种通信协议,它允许将超文本标记语言(HTML)文档从Web服务器传送到 ...