Redis解读(3):Redis分布式锁、消息队列、操作位图进阶应用
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 脚本,大致上两种思路:
- 提前在 Redis 服务端写好 Lua 脚本,然后在 Java 客户端去调用脚本(推荐)。
- 可以直接在 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分布式锁、消息队列、操作位图进阶应用的更多相关文章
- Redis 上实现的分布式锁
转载Redis 上实现的分布式锁 由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多 ...
- 在 Redis 上实现的分布式锁
由于近排很忙,忙各种事情,还有工作上的项目,已经超过一个月没写博客了,确实有点惭愧啊,没能每天或者至少每周坚持写一篇博客.这一个月里面接触到很多新知识,同时也遇到很多技术上的难点,在这我将对每一个有用 ...
- 使用Redis SETNX 命令实现分布式锁
基于setnx和getset http://blog.csdn.net/lihao21/article/details/49104695 使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其 ...
- Redis整合Spring实现分布式锁
spring把专门的数据操作独立封装在spring-data系列中,spring-data-redis是对Redis的封装 <dependencies> <!-- 添加spring- ...
- 使用Redis SETNX 命令实现分布式锁(转载)
使用Redis的 SETNX 命令可以实现分布式锁,下文介绍其实现方法. SETNX命令简介 命令格式 SETNX key value 将 key 的值设为 value,当且仅当 key 不存在. 若 ...
- 基于 Redis 实现简单的分布式锁
摘要 分布式锁在很多应用场景下是非常有效的手段,比如当运行在多个机器上的不同进程需要访问同一个竞争资源的时候,那么就会涉及到进程对资源的加锁和释放,这样才能保证数据的安全访问.分布式锁实现的方案有很多 ...
- 基于Redis实现简单的分布式锁【理论】
摘要 分布式锁在很多应用场景下是非常有效的手段,比如当运行在多个机器上的不同进程需要访问同一个竞争资源的时候,那么就会涉及到进程对资源的加锁和释放,这样才能保证数据的安全访问.分布式锁实现的方案有很多 ...
- 手把手教你用redis实现一个简单的mq消息队列(java)
众所周知,消息队列是应用系统中重要的组件,主要解决应用解耦,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构.目前使用较多的消息队列有 ActiveMQ,RabbitMQ,Zero ...
- Redis、Zookeeper实现分布式锁——原理与实践
Redis与分布式锁的问题已经是老生常谈了,本文尝试总结一些Redis.Zookeeper实现分布式锁的常用方案,并提供一些比较好的实践思路(基于Java).不足之处,欢迎探讨. Redis分布式锁 ...
- Java分布式:消息队列(Message Queue)
Java分布式:消息队列(Message Queue) 引入消息队列 消息,是服务间通信的一种数据单位,消息可以非常简单,例如只包含文本字符串:也可以更复杂,可能包含嵌入对象.队列,是一种常见的数据结 ...
随机推荐
- NXP i.MX 8M Plus工业开发板规格书(四核ARM Cortex-A53 + 单核ARM Cortex-M7,主频1.6GHz)
1 评估板简介 创龙科技TLIMX8MP-EVM是一款基于NXP i.MX 8M Plus的四核ARM Cortex-A53 + 单核ARM Cortex-M7异构多核处理器设计的高性能工业评估板 ...
- java 高效递归查询树 find_in_set 处理递归树
建表语句 DROP TABLE IF EXISTS `sys_dept`; CREATE TABLE `sys_dept` ( `id` bigint(20) NOT NULL AUTO_INCREM ...
- 委托之Action与Func
1 例程代码: using System; using System.Collections.Generic; using System.Linq; using System.Text; using ...
- tp5.1--数据库事务操作
https://blog.csdn.net/qq_42176520/article/details/88708395 使用事务处理的话,需要数据库引擎支持事务处理.比如 MySQL 的 MyISAM ...
- Day 1 - 二分
整数二分 我们可以做到每次排除一半的答案,时间复杂度 \(O(\log n)\). long long l = L, r = R; while(l <= r) { long long mid = ...
- 阅读翻译Prompting Engineering Guides之Introduction(提示工程简介)
阅读翻译Prompting Engineering Guides之Introduction(提示工程简介) 关于 首次发表日期:2024-07-19 Prompting Engineering Gui ...
- SQL Server 图解备份(完全备份、差异备份、增量备份)和还原
常用的数据备份方式有完全备份.差异备份以及增量备份,那么这三种备份方式有什么区别,在具体应用中又该如何选择呢? 1.三种备份方式 完全备份(Full Backup):备份全部选中的文件夹,并不依赖文件 ...
- .NET Core 3.x 基于AspectCore实现AOP,实现事务、缓存拦截器
最近想给我的框架加一种功能,就是比如给一个方法加一个事务的特性Attribute,那这个方法就会启用事务处理.给一个方法加一个缓存特性,那这个方法就会进行缓存.这个也是网上说的面向切面编程AOP. A ...
- CCF 有趣的数
问题描述: 试题编号: 201312-4 试题名称: 有趣的数 时间限制: 1.0s 内存限制: 256.0MB 问题描述: 问题描述 我们把一个数称为有趣的,当且仅当: 1. 它的数字只包含0, 1 ...
- [ABC363G] Dynamic Scheduling 与 P4511 [CTSC2015] 日程管理
思路: 对于插入操作,设插入 \(\{t,p\}\): 若当前 \(1 \sim t\) 有空位,那么就放进去. 否则,\(1 \sim t\) 是被塞满了的: 首先容易想到的是找到 \(1 \sim ...