原文链接: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. 干掉 powerdesigner,设计数据库表用它就够了

    最近有个新项目刚过完需求,正式进入数据库表结构设计阶段,公司规定统一用数据建模工具 PowerDesigner.但我并不是太爱用这个工具,因为它的功能实在是太多了,显得很臃肿,而平时设计表用的也就那么 ...

  2. Android驱动学习-Eclipse安装与配置

    在ubuntu系统下安装配置Eclipse软件.并且让其支持编译java程序和内核驱动程序. 1. 下载Eclipse软件. 打开官网:http://www.eclipse.org/  点击 DOWN ...

  3. JDK下载地址 Oracle JDK下载 地址 (已解决)

    现在JDK开始收费了 Oracle官方对JDK的管理也变得严格了,现在想要在官网下载jdk需要先注册Oracle账号,这倒是小事但是网页反应慢注册填写内容复杂导致很多人不想注册. 不过有的人提供了公开 ...

  4. 异步技巧之CompletableFuture

    摘自--https://juejin.im/post/5b4622df5188251ac9766f47 异步技巧之CompletableFuture 1.Future接口 1.1 什么是Future? ...

  5. python之json、pickle模块

    一.json模块 之前我们学习过用eval内置方法可以将一个字符串转成python对象,不过,eval方法是有局限性的,对于普通的数据类型,json.loads和eval都能用,但遇到特殊类型的时候, ...

  6. LAMP架构之PHP-FPM 服务器 转

    安装PHP 解决依赖关系 # 请配置好yum源(系统安装源及epel源)后执行如下命令: yum -y groupinstall "Desktop Platform Development& ...

  7. 初识 D3.js :打造专属可视化

    一.前言 随着现在自定义可视化的需求日益增长,Highcharts.echarts等高度封装的可视化框架已经无法满足用户各种强定制性的可视化需求了,这个时候D3的无限定制的能力就脱颖而出. 如果想要通 ...

  8. ORA-28001: the password has expired解决方法

    Oracle提示错误消息ORA-28001: the password has expired,是由于Oracle11G的新特性所致, Oracle11G创建用户时缺省密码过期限制是180天(即6个月 ...

  9. SonarQube学习(五)- SonarQube之自定义规则使用

    一.前言 古人云:"欲速则不达",最近真的是深有体会.学习也是如此,不是一件着急的事,越是着急越不会. 就拿SonarQube来说吧,去年年末就想学来着,但是想着想着就搁置了,有时 ...

  10. 【Flutter】容器类组件之装饰容器

    前言 DecoratedBox可以在其子组件绘制前后绘制一些装饰,例如背景,边框,渐变等. 接口描述 const DecoratedBox({ Key key, // 代表要绘制的装饰 @requir ...