原文链接: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)一把简单的“锁”的更多相关文章

  1. Redis分布式锁实现Redisson 15问

    大家好,我是三友. 在一个分布式系统中,由于涉及到多个实例同时对同一个资源加锁的问题,像传统的synchronized.ReentrantLock等单进程情况加锁的api就不再适用,需要使用分布式锁来 ...

  2. MyBatisPlus乐观锁,乐观锁竟然如此简单

    乐观锁 在便是过程中,我们经常会被问到乐观锁,悲观锁,都非常简单 乐观锁:顾名思义,思想十分乐观,总是认为不会出现问题,无论什么都不去上锁!如果出现了问题,就再更新测试 悲观锁:顾明思义,思想十分悲观 ...

  3. 卧槽,redis分布式如果用不好,坑真多

    前言 在分布式系统中,由于redis分布式锁相对于更简单和高效,成为了分布式锁的首先,被我们用到了很多实际业务场景当中. 但不是说用了redis分布式锁,就可以高枕无忧了,如果没有用好或者用对,也会引 ...

  4. Redis分布式锁实现简单秒杀功能

    这版秒杀只是解决瞬间访问过高服务器压力过大,请求速度变慢,大大消耗服务器性能的问题. 主要就是在高并发秒杀的场景下,很多人访问时并没有拿到锁,所以直接跳过了.这样就处理了多线程并发问题的同时也保证了服 ...

  5. 单实例redis分布式锁的简单实现

    redis分布式锁的基本功能包括, 同一刻只能有一个人占有锁, 当锁被其他人占用时, 获取者可以等待他人释放锁, 此外锁本身必须能超时自动释放. 直接上java代码, 如下: package com. ...

  6. 自己写了个简单的redis分布式锁【我】

    自己写了个简单的redis分布式锁 [注意:此锁需要在每次使用前都创建对象,也就是要在线程内每次都创建对象后使用] package redis; import java.util.Collection ...

  7. redis 分布式锁的简单使用

    RedisLock--让 Redis 分布式锁变得简单 目录 1. 项目介绍 2. 快速使用 2.1 引入 maven 坐标 2.2 注册 RedisLock 2.3 使用 3. 参与贡献 4. 联系 ...

  8. Lua脚本在redis分布式锁场景的运用

    目录 锁和分布式锁 锁是什么? 为什么需要锁? Java中的锁 分布式锁 redis 如何实现加锁 锁超时 retry redis 如何释放锁 不该释放的锁 通过Lua脚本实现锁释放 用redis做分 ...

  9. 4、redis 分布式锁

    1. 前言 关于分布式锁的实现,目前常用的方案有以下三类: 数据库乐观锁: 基于分布式缓存实现的锁服务,典型代表有 Redis 和基于 Redis 的 RedLock: 基于分布式一致性算法实现的锁服 ...

随机推荐

  1. [LeetCode]60. Permutation Sequence求全排列第k个

    /* n个数有n!个排列,第k个排列,是以第(k-1)/(n-1)!个数开头的集合中第(k-1)%(n-1)!个数 */ public String getPermutation(int n, int ...

  2. [leetcode]725. Split Linked List in Parts链表分块

    思路很简单  按时链表的题做起来很容易犯小错误,思维要缜密 还要多练习啊 做之前最好画算法框图 public ListNode[] splitListToParts(ListNode root, in ...

  3. SQL数据库创建,创建表,增删改查

    创建数据库:create datebase数据库名 删除数据库:drop datebase 数据库名称 创建表格式: create table 表名(字段名1,字段类型1,字段名2,字段类型2) 查询 ...

  4. java异常相关说明(printStackTrace,fillInStackTrace等)

    我们在实际场景中很容易catch(Exception e) 简单粗暴 这样写代码有几个问题 1.你无法细分具体异常 因为有时需要针对不同异常 产生不同的应对行为 2.直接exception 往往不会包 ...

  5. 第十八章节 BJROBOT 安卓手机 APP 建地图【ROS全开源阿克曼转向智能网联无人驾驶车】

    1.把小车平放在地板上,用资料里的虚拟机,打开一个终端 ssh 过去主控端启动roslaunch znjrobotbringup.launch 2.在虚拟机端再打开一个终端,ssh 过去主控端启动ro ...

  6. vue中的一些用法,持续更新中......

    1.跳转用法 @1.在template模板中通常使用router-link to='url' @2.在js中 1.this.$router.push({path: ''/order/index''}) ...

  7. 使用Lists.partition切分性能优化

    项目实战 影拓邦电影同步中,使用Lists.partition按500条长度进行切分,来实现es的同步. 切分的List为 使用介绍及示例 将list集合按指定长度进行切分,返回新的List<L ...

  8. 关于 RNN 循环神经网络的反向传播求导

    关于 RNN 循环神经网络的反向传播求导 本文是对 RNN 循环神经网络中的每一个神经元进行反向传播求导的数学推导过程,下面还使用 PyTorch 对导数公式进行编程求证. RNN 神经网络架构 一个 ...

  9. MyBatis中id回填的两种方式

    在一种场景下需要刚刚插入数据的ID,如果数据少可以先看数据库,记下ID,但数据很多,假设一万个用户并发,每个用户都插入自己的ID,就很难记下来. 下面给定一个场景: 1 User user = new ...

  10. 阿里云OSS整合

    一.对象存储OSS 为了解决海量数据存储与弹性扩容(主要是静态文件的存储例如图片,语音,视频等),项目中我们通常采用云存储的解决方案- 阿里云OSS. 1.开通"对象存储OSS"服 ...