一、全局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引入的一种新数据类型(即可实现持久化),可以实现一个功能完善的消息队列。