Redis 做分布式锁

分布式锁也算是 Redis 比较常见的使用场景

问题场景:

例如一个简单的用户操作,一个线城去修改用户的状态,首先从数据库中读出用户的状态,然后

在内存中进行修改,修改完成后,再存回去。在单线程中,这个操作没有问题,但是在多线程

中,由于读取、修改、存 这是三个操作,不是原子操作,所以在多线程中,这样会出问题。

对于这种问题,我们可以使用分布式锁来限制程序的并发执行。

1.基本用法

分布式锁实现的思路很简单,就是进来一个线城先占位,当别的线城进来操作时,发现已经有人占位

了,就会放弃或者稍后再试。

在 Redis 中,占位一般使用 setnx 指令,先进来的线城先占位,线城的操作执行完成后,再调用 del 指

令释放位子。

根据上面的思路,我们写出的代码如下:

package org.taoguoguo.distributed.lock;
import org.taoguoguo.redis.Redis; /**
* @author taoguoguo
* @description LockTest
* @website https://www.cnblogs.com/doondo
* @create 2021-04-11 16:19
*/
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
Long setnx = jedis.setnx("lockName", "lockValue");
if(1 == setnx){
//没有线程占位,执行业务代码
jedis.set("name","taoguoguo");
System.out.println(jedis.get("name"));
//释放资源
jedis.del("lockName");
}else{
//有线程占位,停止/暂缓 操作
}
});
}
}

上面的代码存在一个小小问题:如果代码业务执行的过程中抛异常或者挂了,这样会导致 del 指令没有

被调用,这样,lockName 无法释放,后面来的请求全部堵塞在这里,锁也永远得不到释放。

要解决这个问题,我们可以给锁添加一个过期时间,确保锁在一定的时间之后,能够得到释放。改进后

的代码如下:

package org.taoguoguo.distributed.lock;
import org.taoguoguo.redis.Redis; /**
* @author taoguoguo
* @description LockTest
* @website https://www.cnblogs.com/doondo
* @create 2021-04-11 16:19
*/
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
Long setnx = jedis.setnx("lockName", "lockValue");
if(1 == setnx){
//给锁添加一个过期时间,防止应用在运行过程中抛出异常导致锁无法及时得到释放
jedis.expire("lockName",5);
//没有线程占位,执行业务代码
jedis.set("name","taoguoguo");
System.out.println(jedis.get("name"));
//释放资源
jedis.del("lockName");
}else{
//有线程占位,停止/暂缓 操作
}
});
}
}

这样改造之后,还有一个问题,就是在获取锁和设置过期时间之间如果如果服务器突然挂掉了,这个时

候锁被占用,无法及时得到释放,也会造成死锁,因为获取锁和设置过期时间是两个操作,不具备原子

性。

为了解决这个问题,从 Redis2.8 开始,setnx 和 expire 可以通过一个命令一起来执行了,我们对上述

代码再做改进:

package org.taoguoguo.distributed.lock;
import org.taoguoguo.redis.Redis;
import redis.clients.jedis.params.SetParams; /**
* @author taoguoguo
* @description LockTest
* @website https://www.cnblogs.com/doondo
* @create 2021-04-11 16:19
*/
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
String set = jedis.set("lockName", "lockValue", new SetParams().nx().ex(5));
if(set != null && "OK".equals(set)){
//没有线程占位,执行业务代码
jedis.set("name","taoguoguo");
System.out.println(jedis.get("name"));
//释放资源
jedis.del("lockName");
}else{
//有线程占位,停止/暂缓 操作
}
});
}
}

2.解决超时问题

问题场景:

为了防止业务代码在执行的时候抛出异常,我们给每一个锁添加了一个超时时间,超时之后,锁会被自

动释放,但是这也带来了一个新的问题:如果要执行的业务非常耗时,可能会出现紊乱。举个例子:第

一个线程首先获取到锁,然后开始执行业务代码,但是业务代码比较耗时,执行了 8 秒,这样,会在第

一个线程的任务还未执行成功锁就会被释放了,此时第二个线程会获取到锁开始执行,在第二个线程刚

执行了 3 秒,第一个线程也执行完了,此时第一个线程会释放锁,但是注意,它释放的第二个线程的

锁,释放之后,第三个线程进来。

对于这个问题,我们可以从两个角度入手:

  • 尽量避免在获取锁之后,执行耗时操作。
  • 可以在锁上面做文章,将锁的 value 设置为一个随机字符串,每次释放锁的时候,都去比较随机

    字符串是否一致,如果一致,再去释放,否则,不释放。

对于第二种方案,由于释放锁的时候,要去查看锁的 value,第二个比较 value 的值是否正确,第三步

释放锁,有三个步骤,很明显三个步骤不具备原子性,为了解决这个问题,我们得引入 Lua 脚本。

Lua 脚本的优势:

  • 使用方便,Redis 中内置了对 Lua 脚本的支持。

  • Lua 脚本可以在 Redis 服务端原子的执行多个 Redis 命令。

  • 由于网络在很大程度上会影响到 Redis 性能,而使用 Lua 脚本可以让多个命令一次执行,可以有

    效解决网络给 Redis 带来的性能问题。

在 Redis 中,使用 Lua 脚本,大致上两种思路:

  1. 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)。
  2. 可以直接在 Java 端去写 Lua 脚本,写好之后,需要执行时,每次将脚本发送到 Redis 上去执行。

首先在 Redis 服务端创建 Lua 脚本,内容如下:

if redis.call("get",KEYS[1])==ARGV[1] then
  return redis.call("del",KEYS[1])
else
  return 0
end

接下来,可以给 Lua 脚本求一个 SHA1 和,命令如下:

cat releasewherevalueequal.lua | redis-cli -a 123456 script load --pipe

script load 这个命令会在 Redis 服务器中缓存 Lua 脚本,并返回脚本内容的 SHA1 校验和,然后在 Java 端调用时,传入 SHA1 校验和作为参数,这样 Redis 服务端就知道执行哪个脚本了。

接下来,在 Java 端调用这个脚本。

package org.taoguoguo.redis;

import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
import java.util.UUID; /**
* @author taoguoguo
* @description LuaTest
* @website https://www.cnblogs.com/doondo
* @create 2021-04-11 17:56
*/
public class LuaTest {
public static void main(String[] args) {
Redis redis = new Redis();
for (int i=0; i<10; i++){
redis.execute(jedis -> {
//1.先获取一个随机字符串
String value = UUID.randomUUID().toString();
//2.获取锁
String lock = jedis.set("lockName", value, new SetParams().nx().ex(5));
//3.判断是否成功拿到锁
if (lock != null && "OK".equals(lock)) {
//4.具体的业务操作
jedis.set("site", "https://www.cnblogs.com/doondo");
String site = jedis.get("site");
System.out.println(site);
//5.释放锁
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("lockName"), Arrays.asList(value));
} else {
System.out.println("没拿到锁");
}
});
}
}
}

Redis 做消息队列

我们平时说到消息队列,一般都是指 RabbitMQ、RocketMQ、ActiveMQ 以及大数据里边的 Kafka,

这些是我们比较常见的消息中间件,也是非常专业的消息中间件,作为专业的中间件,它里边提供了许

多功能。

但是,当我们需要使用消息中间件的时候,并非每次都需要非常专业的消息中间件,假如我们只有一个

消息队列,只有一个消费者,那就没有必要去使用上面这些专业的消息中间件,这种情况我们可以直接

使用 Redis 来做消息队列。

Redis 的消息队列不是特别专业,他没有很多高级特性,适用简单的场景,如果对于消息可靠性有着极

高的追求,那么不适合使用 Redis 做消息队列。

1.消息队列

Redis 做消息队列,使用它里边的 List 数据结构就可以实现,我们可以使用 lpush/rpush 操作来实现入

队,然后使用 lpop/rpop 来实现出队。

回顾一下:

在客户端(例如 Java 端),我们会维护一个死循环来不停的从队列中读取消息,并处理,如果队列中

有消息,则直接获取到,如果没有消息,就会陷入死循环,直到下一次有消息进入,这种死循环会造成

大量的资源浪费,这个时候,我们可以使用之前讲的 blpop/brpop 。

2.延迟消息队列

延迟队列可以通过 zset 来实现,因为 zset 中有一个 score,我们可以把时间作为 score,将 value 存到

redis 中,然后通过轮询的方式,去不断的读取消息出来。

首先,如果消息是一个字符串,直接发送即可,如果是一个对象,则需要对对象进行序列化,这里我们

使用 JSON 来实现序列化和反序列化。

所以,首先在项目中,添加 JSON 依赖:

<dependency>
   <groupId>com.fasterxml.jackson.core</groupId>
   <artifactId>jackson-databind</artifactId>
   <version>2.10.3</version>
</dependency>

接下来,构造一个消息对象:

package org.taoguoguo.message;

/**
* @author taoguoguo
* @description RedisMessage 消息对象
* @website https://www.cnblogs.com/doondo
* @create 2021-04-12 20:33
*/
public class RedisMessage { //消息ID
private String id;
//消息体
private Object data; public String getId() {
return id;
} public void setId(String id) {
this.id = id;
} public Object getData() {
return data;
} public void setData(Object data) {
this.data = data;
} @Override
public String toString() {
return "RedisMessage{" +
"id='" + id + '\'' +
", data=" + data +
'}';
}
}

接下来封装一个消息队列:

package org.taoguoguo.message;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import redis.clients.jedis.Jedis; import java.util.Date;
import java.util.Set;
import java.util.UUID; /**
* @author taoguoguo
* @description DelayMessageQueue 延迟消息队列
* @website https://www.cnblogs.com/doondo
* @create 2021-04-12 20:35
*/
public class DelayMessageQueue { private Jedis jedis;
//消息队列队列名
private String queue; public DelayMessageQueue(Jedis jedis, String queue) {
this.jedis = jedis;
this.queue = queue;
} /**
* 消息入队
* @param data 要发送的消息
*/
public void queue(Object data){
try {
//1.构造一个Redis消息对象
RedisMessage message = new RedisMessage();
message.setId(UUID.randomUUID().toString());
message.setData(data);
//2.序列化
String jsonMessage = new ObjectMapper().writeValueAsString(message);
System.out.println("Redis Message publish: " + new Date());
//消息发送,score 延迟 5 秒
jedis.zadd(queue, System.currentTimeMillis()+5000,jsonMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
} /**
* 消息消费
*/
public void loop(){
//当前线程未被打断 一直监听
while (!Thread.interrupted()){
//读取 score 在 0 到当前时间戳之间的消息 一次读取一条,偏移量为0
Set<String> messageSet = jedis.zrangeByScore(queue, 0, System.currentTimeMillis(), 0, 1);
if(messageSet.isEmpty()){
try {
//如果消息是空的,则休息 500 毫秒然后继续
Thread.sleep(500);
} catch (InterruptedException e) {
//如果抛出异常 退出循环
break;
}
continue;
}
//如果读取到了消息,则直接读取出来
String messageStr = messageSet.iterator().next();
if(jedis.zrem(queue,messageStr) > 0){
//消息存在,并且消费成功
try {
RedisMessage redisMessage = new ObjectMapper().readValue(messageStr, RedisMessage.class);
System.out.println("Redis Message receive: " + new Date() + redisMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
}
} }

测试:

package org.taoguoguo.message;

import org.taoguoguo.redis.Redis;

/**
* @author taoguoguo
* @description DelayMessageTest
* @website https://www.cnblogs.com/doondo
* @create 2021-04-12 21:20
*/
public class DelayMessageTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis -> {
//构造一个消息队列
DelayMessageQueue queue = new DelayMessageQueue(jedis, "taoguoguo-delay-queue");
//构造消息生产者
Thread producer = new Thread(){
@Override
public void run() {
for(int i=0;i<5;i++){
queue.queue("https://www.cnblogs.com/doondo>>>>>"+i);
}
}
}; //构造消息消费者
Thread consumer = new Thread(){
@Override
public void run() {
queue.loop();
}
}; //启动
producer.start();
consumer.start();
//消费完成后,停止程序,时间大于消费时间
try {
Thread.sleep(10000);
consumer.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}

Redis操作位图

1.基本介绍

用户一年的签到记录,如果你用 string 类型来存储,那你需要 365 个 key/value,操作起来麻烦。通过位图可以有效的简化这个操作。

它的统计很简单:01111000111

每天的记录占一个位,365 天就是 365 个位,大概 46 个字节,这样可以有效的节省存储空间,如果有一天想要统计用户一共签到了多少天,统计 1 的个数即可。

对于位图的操作,可以直接操作对应的字符串(get/set),可以直接操作位(getbit/setbit)

2.基本操作

2.1零存整取

存的时候操作的是位,获取的时候是获取整个字符串

例如存储一个 Java 字符串:

字符 ASCII 二进制
J 74 01001010
a 97 01100001
v 118 01110110

2.2整存零取

存一个字符串进去,但是通过位操作获取字符串。

3.统计

例如签到记录:01111000111

1 表示签到的天,0 表示没签到,统计总的签到天数:可以使用 bitcount。

bitcount 中,可以统计的起始位置,但是注意,这个起始位置是指字符的起始位置而不是 bit 的起始位置。

除了 bitcount 之外,还有一个 bitpos。bitpos 可以用来统计在指定范围内出现的第一个 1 或者 0 的位置,这个命令中的起始和结束位置都是字符索引,不是 bit 索引,一定要注意。

4.Bit 批处理

在 Redis 3.2 之后,新加了一个功能叫做 bitfiled ,可以对 bit 进行批量操作。

例如:

BITFIELD name get u4 0

表示获取 name 中的位,从 0 开始获取,获取 4 个位,返回一个无符号数字。

  • u 表示无符号数字
  • i 表示有符号数字,有符号的话,第一个符号就表示符号位,1 表示是一个负数。

BITFIELD 也可以一次执行多个操作。

GET(对于结果不太明白的,学习一下计算机中 位与字节、以及进制之间的关系)

可以一次性进行多个GET

SET:

用无符号的 98 转成的 8 位二进制数字,代替从第 8 位开始接下来的 8 位数字。

INCRBY:

对置顶范围进行自增操作,自增操作可能会出现溢出,既可能是向上溢出,也可能是向下溢出。Redis 中对于溢出的处理方案是折返。8 位无符号数 255 加 1 溢出变为 0;8 位有符号数 127,加 1 变为 - 128。

也可以修改默认的溢出策略,可以改为 fail ,表示执行失败。

BITFIELD name overflow fail incrby u2 6 1

sat 表示留在在最大/最小值。

BITFIELD name overflow sat incrby u2 6 1

Redis解读(3):Redis分布式锁、消息队列、操作位图进阶应用的更多相关文章

  1. Redis 上实现的分布式锁

    转载Redis 上实现的分布式锁 由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多 ...

  2. 在 Redis 上实现的分布式锁

    由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用 ...

  3. 使用Redis SETNX 命令实现分布式锁

    基于setnx和getset http://blog.csdn.net/lihao21/article/details/49104695 使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其 ...

  4. Redis整合Spring实现分布式锁

    spring把专门的数据操作独立封装在spring-data系列中,spring-data-redis是对Redis的封装 <dependencies> <!-- 添加spring- ...

  5. 使用Redis SETNX 命令实现分布式锁(转载)

    使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法. SETNX命令简介 命令格式 SETNX key value 将 key 的值设为 value,当且仅当 key 不存在. 若 ...

  6. 基于 Redis 实现简单的分布式锁

    摘要 分布式锁在很多应用场景下是非常有效的手段,比如当运行在多个机器上的不同进程需要访问同一个竞争资源的时候,那么就会涉及到进程对资源的加锁和释放,这样才能保证数据的安全访问.分布式锁实现的方案有很多 ...

  7. 基于Redis实现简单的分布式锁【理论】

    摘要 分布式锁在很多应用场景下是非常有效的手段,比如当运行在多个机器上的不同进程需要访问同一个竞争资源的时候,那么就会涉及到进程对资源的加锁和释放,这样才能保证数据的安全访问.分布式锁实现的方案有很多 ...

  8. 手把手教你用redis实现一个简单的mq消息队列(java)

    众所周知,消息队列是应用系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构.目前使用较多的消息队列有 ActiveMQ,RabbitMQ,Zero ...

  9. Redis、Zookeeper实现分布式锁——原理与实践

    Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...

  10. Java分布式:消息队列(Message Queue)

    Java分布式:消息队列(Message Queue) 引入消息队列 消息,是服务间通信的一种数据单位,消息可以非常简单,例如只包含文本字符串:也可以更复杂,可能包含嵌入对象.队列,是一种常见的数据结 ...

随机推荐

  1. C#/.NET/.NET Core优秀项目和框架2024年6月简报

    前言 公众号每月定期推广和分享的C#/.NET/.NET Core优秀项目和框架(每周至少会推荐两个优秀的项目和框架当然节假日除外),公众号推文中有项目和框架的介绍.功能特点.使用方式以及部分功能截图 ...

  2. 3568F-物联网模块开发案例

  3. 【Error】mysql的error.log中ranges: 268 max_threads: 4 split: 268 depth: 2是什么意思?

    2021-12-08T09:36:39.612332+08:00 44213799 [Note] [MY-011825] [InnoDB] Parallel scan: 4 2021-12-08T09 ...

  4. Java基础:线程的三种创建方式

    一.继承Thread类 定义一个类继承线程类Thread 重写run()方法 创建线程对象 调用线程对象的start()方法创建线程 Thread类的常用API setName(String name ...

  5. ARC108C

    考虑一颗树怎么染色. 每个子节点染成边的颜色,如果与父亲节点相同,就随便染色(这条边的限制已经被父亲节点满足). 那么一定可以染色. 所以把原图跑最小生成树再按上述方法染色即可. 倘若原图不连通,那么 ...

  6. 使用gitea搭建源码管理【0到1架构系列】

    使用开源搭建Git源码方案,gitlab和gitea是两个不错的方案,gitlab以前简单易用,现在功能复杂且对开源并不友好,gitea一直保持功能单一易用且完全开源,个人推荐gitea. 通过容器安 ...

  7. 新版SpringBoot-Spring-Mybatis事务控制

    快速创建SpringBoot+Spring+Mybatis项目 https://start.spring.io 删除pom中mysql依赖的runtime pom.xml中添加druid依赖 < ...

  8. Linux 文件夹和文件操作【Linux 常用命令系列一】

    〇.前言 本文首先介绍了 Linux 中文件的结构,将全部文件夹罗列并介绍了大概的用途: 然后通过实例介绍了文件夹相关的常用操作,仅供参考. 一.Linux 系统的文件结构 列一下系统全部文件夹: / ...

  9. [oeasy]python0079_控制序列_光标位置设置_ESC_逃逸字符_CSI

    光标位置 回忆上次内容 上次我们研究的比较杂 类型转化 进制转化 捕获异常 版本控制 生成帮助文档 变量的常用类型 变量的生命周期控制   数据类型主要研究了两个 字符串 str   整型数字 int ...

  10. JMeter 逻辑控制之IF条件控制器

    逻辑控制之IF条件控制器 测试环境 JMeter-5.4.1 循环控制器介绍 添加While Controller 右键线程组->添加->逻辑控制器->While控制器 控制器面板介 ...