Redis分布式锁

一、全局ID生成器

1.1 概念

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具。具有以下特点:

(1)唯一性;(2)高可用;(3)高性能;(4)递增性;(5)安全性。

1.2 实现

1.2.1 UUID

返回结果是为字符串,不是自增。使用较少。

1.2.2 Redis自增

1.2.3 snowflake算法

1.2.4 数据库自增

二、集群下的线程并发安全问题

通过加锁机制(synchronize)可以解决单机情况下的线程安全问题,但是在集群模式下就不行了。

修改nginx的配置文件nginx.conf:

说明:上图中的配置说明"/api"请求会转到"/backend"请求,且配置了两个服务(172.0.0.1:8081与172.0.0.1:8082)实现负载均衡,即实现集群模式。

使用锁下单(确保一个用户只能下一单):

但是若同一用户发了两次请求,一次请求发送到8081服务,另一次发送到8082服务,可以发现两次请求都获取到了锁,且都完成了下单,导致不符合业务要求。

原因是锁监视器存在机器本地的JVM中,而一台机器有自己的JVM,锁机制在集群模式下失效。该问题用分布式锁来实现。

三、分布式锁

分布式锁:满足分布式模式或集群模式下多进程可见并且互斥的锁。

3.1 三种实现方法

3.2 基于Redis的分布式锁

3.2.1 思路

注意:需要设置key的生存时间,防止服务宕机导致锁未释放。

3.2.2 代码实现

接口:

实现类:


使用锁:

3.2.3 锁误删的问题

3.2.3.1 产生原因

以上图图示说明,线程1获取了锁,但是执行时线程阻塞了,导致锁超时释放。而此时线程2去获取了锁。在线程2执行过程中,线程1执行完并去释放锁,实际上释放的是线程2设置的锁,即导致了锁误删。

3.2.3.2 流程修改方法

原流程:

修改后流程:

3.2.3.3 代码实现


3.2.4分布式锁的原子性问题

3.2.3中的锁误删问题的解决方法还有一个问题是,当线程1要删锁时,判断完锁是自己后,阻塞了一段时间,锁超时释放。之后线程2获取了锁,线程1阻塞结束继续执行,进行删锁,又导致了锁误删。因此,判断锁是否是自己的与删除锁的操作需要具备原子性。

3.2.5 Lua脚本解决多条命令原子性问题

Redis提供了Lua脚本(一种编程语言),在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

Lua变成语言的基本语法的参考网站:

bash 复制代码
https://www.runoob.com/lua/lua-tutorial.html

执行脚本指令:

eval

例如:

Lua脚本编写如下:

java的执行redis的lua脚本的接口:

在项目中新建脚本文件:

加载脚本(提前加载好):

3.3 基于Redis分布式锁的问题

注意:上述四个问题出现的概率极低,不考虑这些问题的锁的性能也够用了。

四、Redisson

4.1 Redisson介绍

Redisson是封装好的实现Redis分布式锁的依赖,可以直接使用(意思是前面实现锁的代码其实都不用自己写)。使用方法如下。

4.1.1 引入依赖

4.1.2 配置Redisson客户端

有两个配置方式,一种是使用java配置进行配置;一种是使用配置文件与springboot整合实现,并且官方还提供一种springboot start。建议使用第一种方法。

4.1.3 使用Redisson的分布式锁

例如:

4.2 Redisson可重入锁原理

之前介绍的自定义锁未实现可重入机制。Redisson可实现。

例如:

实现机制是当获取锁时判断获取锁的是否是当前线程。

4.3 Redisson分布式锁锁重试问题

4.4 Redisson分布式锁主从一致性问题

4.4.1 问题产生原因

首先,redis的主节点获取锁后,开始向从节点同步信息,但就在此时主节点发生了故障即同步尚未完成。Redis有哨兵监控集群状态,当它发现主节点宕机后,从从节点种选择一个作为主节点。Java应用去访问新的主节点时发现锁已丢失,即锁失效了,此时再有其他线程来获取锁还是可以成功的,这就会产生线程并发安全的问题即主从一致性导致的锁失效问题。

4.4.2 Redisson解决方案

Redisson将所有的节点都看成独立的redis节点,相互之间没有主从关系。此时获取锁的方式就变了,之前是找到master节点然后获取锁;但现在必须依次向多个redis节点都去获取锁,这些都保存了锁标识才算获取锁成功(即使用联锁)。

也可以保留主从节点关系,只是有多个主节点:

不具备主从关系的代码实现:

三个独立的redisclient,

创建联锁,

4.5 使用JVM阻塞线程

未使用redis缓存的秒杀下单流程如下。

如上图,对流程进行优化。"判断秒杀库存"和"校验一人一单"由redis完成,再通过消息队列完成下单操作。

具体实现:

代码实现:

新增秒杀优惠券的同时,将优惠券信息保存到redis中,

基于lua脚本,实现秒杀库存、一人一单,决定用户是否抢购成功,

如果抢购成功,将优惠券id和用户id封装后存入阻塞队列,

开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能,

说明:上图中的线程的任务是从阻塞队列中不断地取出订单。


五、消息队列

4.5中使用JVM线程可能造成内存不足的情况,且存在数据安全问题(重启或宕机后阻塞队列中的信息全部丢失)。

5.1 基于List结构模拟消息队列


使用BRPOP指令。

优缺点:

5.2 基于PubSub的消息队列



5.3 基于Steam的消息队列

Stream是Redis 5.0引入的一种新数据类型(即可实现持久化),可以实现一个功能完善的消息队列。

相关推荐
夜泉_ly43 分钟前
MySQL -安装与初识
数据库·mysql
power-辰南2 小时前
高并发系统架构设计全链路指南
分布式·系统架构·高并发·springcloud
qq_529835352 小时前
对计算机中缓存的理解和使用Redis作为缓存
数据库·redis·缓存
月光水岸New4 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6754 小时前
数据库基础1
数据库
我爱松子鱼4 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo5 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser6 小时前
【SQL】多表查询案例
数据库·sql
Galeoto6 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
希忘auto6 小时前
详解Redis在Centos上的安装
redis·centos