Nginx 篇
【1】简述一下什么是Nginx,它有什么优势和功能?
Nginx 是高性能的 HTTP 和反向代理的服务器,处理高并发能力是十分强大的,能经受高负载的考验,有报告表明能支持高达 50,000 个并发连接数。Nginx主要提供功能有
-
http服务器
-
反向代理服务器
-
负载均衡服务器
-
动静分离配置
-
缓存数据
【2】简述一下什么是正向代理,什么是反向代理
正向代理代理的是客户端访问服务端,比如防火墙,反向代理代理的是服务端,等待客户端访问代理服务。具体配置如下:
【3】解释一下什么是Nginx的负载均衡
nginx反向代理tomcat服务集群,当客户端访问nginx服务器时,由nginx负载均衡去访问tomcat集群中的某一个节点。具体配置:
【4】说一下什么是动静分离
静态资源配置到nginx服务器中,动态资源通过nginx反向代理到tomcat。
Docker 篇
【1】说一下什么是Docker?
Docker是一个开源的应用容器引擎,它让开发者可以将他们的应用以及依赖包打包到一个可移植的镜像中,然后发布到任何流行的Linux或Windows操作系统的机器上。Docker使用沙箱机制,使得容器之间完全隔离,互不干扰。Docker的镜像可以包含应用程序的代码、运行环境、依赖库、配置文件等必需的资源,从而实现方便快速并且与平台解耦的自动化部署方式。无论部署环境如何,容器中的应用程序都会运行在同一种环境下。
【2】说一下你都用过哪些Docker命令
Docker命令操作主要包含
-
镜像操作
-
容器操作
-
数据卷操作
-
自定义镜像操作
-
网络操作
【3】说一下镜像操作的常用命令
-
docker pull 拉取镜像
-
docker push 推送镜像
-
docker images 查看所有镜像
-
docker inspect 镜像名 查看镜像详细信息
-
docker rmi 镜像名 删除镜像
-
docker build 自定义镜像
-
docker save 保存镜像
-
docker load 加载镜像
【4】说一下Docker容器命令
-
docker run 构建容器,常见参数
-
--name 容器名
-
-d 后台启动
-
-p 端口映射
-
-v 数据卷挂载
-
-e 设置传递环境变量
-
-i 以交互模式运行容器,通常与 -t 同时使用;
-
-t 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
-
-it 以交互模式运行容器,通常与-d 一起使用
-
--restart 设置容器的重启策略
-
--network 将容器连接到指定网络
-
-
docker start 容器名 启动容器
-
docker stop 容器名 停止容器
-
docker restart 容器名 重启容器
-
docker ps 查看所有正在运行的容器
-
docker ps -a 查看所有容器
-
docker rm 删除容器
【5】说一下数据卷命令
数据卷是一个可供一个或多个容器使用的特殊目录,数据卷可以在容器之间共享和重用
对数据卷的修改会立马生效 - 对数据卷的更新,不会影响镜像 - 数据卷默认会一直存在,即使容器被删除 Docker中提供了两种挂载方式,-v和-mount
方式一:
docker run --name xxxx -p 8888:8888 -v /my:/docker -it imagename /bin/bash
--name
: 为容器起名字
-p
:宿主机端口:docker端口 (端口映射)
-v
:宿主机目录:docker目录 (必须为绝对路径)
方式二:
docker run --name xxxx -p 8888:8888 --mount type:volume,source=/src/xxx,target=/xxx /my:/docker -it imagename /bin/bash
注意:使用-v参数时如果本地目录不存在Docker会自动为你创建一个文件夹。使用--mount
参数时如果本地目录不存在,Docker会报错。Docker挂载主机目录的默认权限是读写,用户也可以通过增加readonly指定为只读
-
docker volume create 创建数据卷
-
docker volume ls 查看所有数据卷
-
docker volume inspect 查看数据卷详细信息
-
docker volume rm 删除数据卷
-
docker volume prune 删除未使用的数据卷
【6】说一下如何自定义镜像
首先编写Dockerfile,Dockerfile内容参照相关文档操作,最后根据docker build -t 镜像名 . 创建镜像。
【7】说一下如何创建网络
-
docker network connect 将容器连接到网络
-
docker network create 创建一个网络
-
docker network ls 列出所有网络
-
docker network inspect 显示一个或多个网络详细信息
-
docker network rm 删除一个获得多个网络
-
docker network prune 删除所有未使用的网络
Zookeeper 篇
【1】说一下什么是Zookeeper ?
ZooKeeper是一个开放源代码的分布式协调服务。ZooKeeper的设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。
Zookeeper主要能作为分布式架构的协调者---注册中心,以及大数据的协调者(了解)
Zookeeper 是一个树形结构的监听机制系统,主要应用场景:
-
数据发布/订阅
-
负载均衡
-
服务命名
-
分布式协调/通知
【2】说一下你都用过哪些zookeeper命令
节点为键值的形式
-
create 创建节点
-
ls 查看节点
-
get /获取节点结构
-
set 设置节点命令
-
delete 删除节点
-
deleteall 删除节点以及节点包含内容
-
stat 查看节点状态
【3】zookeeper的作用
1.Zookeeper加强集群稳定性 通过层级命名空间让分布式进程互相协同工作。这就意味着zookeeper有着高吞吐和低延迟。 Zookeeper实现了高性能,高可靠性,和有序的访问。
2.Zookeeper加强集群持续性
组成Zookeeper的各个服务器必须要能相互通信。他们在内存中保存了服务器状态,也保存了操作的日志,并且持久化快照。只要大多数的服务器是可用的,那么Zookeeper就是可用的。
客户端连接到一个Zookeeper服务器,并且维持TCP连接。并且发送请求,获取回复,获取事件,并且发送连接信号。如果这个TCP连接断掉了,那么客户端可以连接另外一个服务器。
3.Zookeeper保证集群有序性 Zookeeper使用数字来对每一个更新进行标记。这样能保证Zookeeper交互的有序。后续的操作可以根据这个顺序实现诸如同步操作这样更高更抽象的服务。
4.Zookeeper保证集群高效 Zookeeper的高效更表现在以读为主的系统上。Zookeeper可以在千台服务器组成的读写比例大约为10:1的分布系统上表现优异
Dubbo 篇
【1】说一下Dubbo执行流程
Dubbo的启动和执行流程 提供者端启动,容器负责把Service信息加载,并通过Protocol注册到注册中心 消费者端启动,通过监听提供者列表感知提供者信息,并在提供者发生改变时,通过注册中心及时通知消费端 消费方通过Proxy模块发起请求 利用Cluster模块来选择真实的要发送给提供者的信息 交由Consumer中的Protocol把信息发送给提供者 提供者同样需要通过Protocol模块来处理消费者信息 最后由真正的服务提供者Service进行处理
【2】说一下Dubbo支持哪些协议?
Dubbo 支持如下协议:
-
Dubbo协议
-
Hessian协议
-
HTTP协议
-
RMI协议
-
WebService协议
-
Memcached协议
-
Redis协议
建议使用Dubbo协议。
【3】注册中心挂了,consumer 还能不能调用 provider?
可以。因为刚开始初始化的时候,consumer 会将需要的所有提供者的地址等信息拉取到本地缓存,所以注册中心挂了可以继续通信。但是 provider 挂了,那就没法调用了。
【4】怎么实现动态感知服务下线的呢?
服务订阅通常有 pull 和 push 两种方式:
-
pull 模式需要客户端定时向注册中心拉取配置;
-
push 模式采用注册中心主动推送数据给客户端。
Dubbo ZooKeeper 注册中心采用是事件通知与客户端拉取方式。服务第一次订阅的时候将会拉取对应目录下全量数据,然后在订阅的节点注册一个 watcher。一旦目录节点下发生任何数据变化, ZooKeeper 将会通过 watcher 通知客户端。客户端接到通知,将会重新拉取该目录下全量数据, 并重新注册 watcher。利用这个模式,Dubbo 服务就可以做到服务的动态发现。
注意:ZooKeeper 提供了“心跳检测”功能,它会定时向各个服务提供者发送一个请求(实际上建立 的是一个 socket 长连接),如果长期没有响应,服务中心就认为该服务提供者已经“挂了”,并将其 剔除。
【5】Dubbo 负载均衡策略?
-
随机(默认):随机来
-
轮训:一个一个来
-
活跃度:机器活跃度来负载
-
一致性 hash:落到同一台机器上
【6】Dubbo 容错策略
-
failover cluster 模式
provider 宕机重试以后,请求会分到其他的 provider 上,默认两次,可以手动设置重试次数,建议把写操作重试次数设置成 0。
-
failback 模式
失败自动恢复会在调用失败后,返回一个空结果给服务消费者。并通过定时任务对失败的调用进行重试,适合执行消息通知等操作。
-
failfast cluster 模式
快速失败只会进行一次调用,失败后立即抛出异常。适用于幂等操作、写操作,类似于 failover cluster 模式中重试次数设置为 0 的情况。
-
failsafe cluster 模式
失败安全是指,当调用过程中出现异常时,仅会打印异常,而不会抛出异常。适用于写入审计日志等操作。
-
forking cluster 模式
并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。
-
broadcacst cluster 模式
广播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。
Redis 篇
【1】为什么要使用Redis缓存,你都用过redis做什么?
答:Redis是一个分布式的缓存中间件,使用缓存我从三个方向介绍使用的原因
-
缓存可以提高网站并发读写能力。
-
缓存可以解决跨进程的内存计算问题
-
可以解决跨进程的分布锁问题
比如,一个电商的首页需要的并发量非常高,我们如何提供首页的并发能力呢,其根本思路就是减少用户对tomcat服务器的访问压力。用户访问首先从nginx缓存获取数据,如果获取不到通过lua脚本查询Redis,如果Redis有数据就将数据同步到nginx缓存中,然后响应客户端。如果redis没有数据才会查询mysql数据库。使用nginx+redis两层缓存的目的一是提高并发量,二是减少缓存雪崩。
在例如缓存可以解决内存计算问题,比如一个网站上传的图片到七牛云,只有保存到数据库的图片才是有用的图片,但操作七牛云和操作数据库的是两个JVM进程时如何计算呢?把两者数据读取到redisset集合,求两个集合的差值就是垃圾图片,然后调用api删除这些图片。在比如商城的购物车数据库是跨JVM进程的,如何存储计算呢?可以通过redis的hash结构存储并计算。
在比如跨进程操作共享数据,如何解决多线程安全问题呢,此时JVM线程锁已经失效,可以使用RedissonClient提供的分布式锁实现,商城的超卖问题就可以通过这个解决。
【2】Redis有哪些数据类型?以及各自的使用场景
答:String 类型、List类型、Set类型、Hash类型、Zset类型、Bitmaps类型、Geospatial类型、HyperLogLog类型。
String类型是最基本类型key-value,可以用于数据缓存,做计数器(incrby),求粉丝数,分布式锁的底层原理(setnx)
List类型是一个双向链表结构,可以用于消息队列,最新列表等能。
Set类型是一个无序集合,自动去重复,可以做一些去重复计算,集合计算等。
Hash类型底层是Hash结构,可以用于对象存储并计算,比如用来存购物车数据。
Zset是一个有权重的Set集合,可以利用权重做些排行榜等需求。
Bitmaps是一个直接用二进制表示数据,并存储的类型,因每位只能存两种情况,可以用来实现考勤统计情况
Geospatial是坐标类型,可以实现地图坐标计算等功能。
HyperLogLog是一个基于基数计算的数据类型,可以用来统计基数,如PV、UV统计这些。
【3】为什么 Redis 单线程模型效率也能那么高?
-
C语言实现,效率高
-
纯内存操作
-
基于非阻塞的IO复用模型机制
-
单线程的话就能避免多线程的频繁上下文切换问题
-
丰富的数据结构(全称采用hash结构,读取速度非常快,对数据存储进行了一些优化,比如亚索表,跳等)
补充:redis请求处理是多线程地,命令处理是单线程的。
【4】说说什么是Redis的发布订阅?
Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息
相关命令
-
subscribe 主题名字 订阅主题
-
publish 主题名字 消息 发送消息
如果发布者发布频道消息时,订阅者还有没有订阅频道是收不到消息的。
【5】如何解决redis的慢查询问题?
慢查询,顾名思义就是比较慢的查询,可以通过慢查询日志获得问题是所在,相关命令:
-
showlog get n 获得前n条慢查询日志
-
showlog len 获得慢查询日志长度
-
config set slowlog-log-slower-than 1000 设置慢查询阀值 单位微妙
-
config set slowlog-max-len 1200 设置慢查询命令长度
-
config rewrire 保存设置
【6】 pipeline 有什么好处,为什么要用 pipeline?
使用 pipeline(管道)的好处在于可以将多次 I/O 往返的时间缩短为一次,但是要求管道中执行的指令间没有因果关系。
用 pipeline 的原因在于可以实现请求/响应服务器的功能,当客户端尚未读取旧响应时,它也可以处理新的请求。如果客户端存在多个命令发送到服务器时,那么客户端无需等待服务端的每次响应才能执行下个命令,只需最后一步从服务端读取回复即可。
【7】说说Redis的持久化技术
Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。 实现:单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。
RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
AOF:Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。 当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。
用AOF来保证数据不丢失,作为数据恢复的第一选择,用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复。
【8】Redis 报内存不足怎么处理?
Redis 内存不足可以这样处理:
-
修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存;
-
设置缓存淘汰策略,提高内存的使用效率;
-
使用 Redis 集群模式,提高存储量。
【9】说说Redis内存淘汰策略?
当 Redis 内存使用达到最大内存限制时,如果继续进行写入操作会导致 Redis 服务崩溃。因此,为了保证 Redis 服务的稳定性,Redis 在内存使用达到最大限制时采取一系列措施,如内存淘汰、警告等。
Redis 内存淘汰策略主要有 6 种:
1.noeviction
这是 Redis 的默认策略,这意味着当内存满了时,任何写操作(如 SET,HSET 等)都会返回一个错误,这种情况下,需要管理员手动清除一些键值对或者增加内存才能继续进行写操作。
2.allkeys-lru
该策略会针对所有的键使用 LRU(最近最少使用)算法来淘汰键值对。对于 Redis 的所有键,按照最近最少使用的原则,优先淘汰那些最近没有使用或很久没有使用的键值对。
# 配置为 allkeys-lru 策略
maxmemory-policy allkeys-lru
3.volatile-lru
在这种模式下,Redis 会选择过期时间较近的键来淘汰。如果一个键设置了过期时间,并且距离过期时间较远,那么 Redis 不会考虑淘汰该键值对。相反,如果一个键离过期时间只有很短的一段时间,那么 Redis 会优先淘汰该键值对
# 配置为 volatile-lru 策略
maxmemory-policy volatile-lru
4.allkeys-random
在这种模式下,Redis 会随机地淘汰一些键值对,重点考虑键值对的访问频率以及键空间。
# 配置为 allkeys-random 策略
maxmemory-policy allkeys-random
5.volatile-random
同 allkeys-random 模式类似,但它只针对开启过期时间的键空间进行淘汰。
volatile-random
同 allkeys-random 模式类似,但它只针对开启过期时间的键空间进行淘汰。
6.volatile-ttl
在这种模式下,Redis 会选择最接近超时的键来淘汰。它与 volatile-lru 类似,不同之处在于它侧重于超时的键而非最近最少使用的键。
# 配置为 volatile-ttl 策略
maxmemory-policy volatile-ttl
7.volatile-lfu
该策略中采用的是 LFU(最不常用)算法。Redis 会优先淘汰使用频率最少的键值对。
# 配置为 volatile-lfu 策略
maxmemory-policy volatile-lfu
【10】Redis 缓存刷新策略有哪些?
【11】怎么使用 Redis 实现消息队列?
一般使用 list 结构作为队列, rpush 生产消息, lpop 消费消息。当 lpop 没有消息的时候,要适当sleep 一会再重试。
-
面试官可能会问可不可以不用 sleep 呢?list 还有个指令叫 blpop ,在没有消息的时候,它会阻塞住直到消息到来。
-
面试官可能还问能不能生产一次消费多次呢?使用 pub / sub 主题订阅者模式,可以实现 1:N 的消息队列。
-
面试官可能还问 pub / sub 有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用 专业的消息队列如 rabbitmq 等。
-
面试官可能还问 Redis 如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset ,拿时间戳作为 score ,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。
面试扩散:很多面试官上来就直接这么问: Redis 如何实现延时队列?
【12】说说你对Redis事务的理解
Redis 中的事务是一组命令的集合,是 Redis 的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会序列化、按顺序地执行。服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。它的原理是先将属于一个事务的命令发送给 Redis,然后依次执行这些命令。
Redis 事务的注意点有哪些?
-
Redis 事务是不支持回滚的,不像 MySQL 的事务一样,要么都执行要么都不执行;
-
Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
-
Redis 事务为什么不支持回滚?
Redis 的事务不支持回滚,但是执行的命令有语法错误,Redis 会执行失败,这些问题可以从程序层 面捕获并解决。但是如果出现其他问题,则依然会继续执行余下的命令。这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
【13】什么是 bigkey?会存在什么影响?
bigkey 是指键值占用内存空间非常大的 key。例如一个字符串 a 存储了 200M 的数据。 bigkey 的主要影响有:
-
网络阻塞;获取 bigkey 时,传输的数据量比较大,会增加带宽的压力。
-
超时阻塞;因为 bigkey 占用的空间比较大,所以操作起来效率会比较低,导致出现阻塞的可能性增加。
-
导致内存空间不平衡;一个 bigkey 存储数据量比较大,同一个 key 在同一个节点或服务器中存储,会造成一定影响
【14】熟悉哪些 Redis 集群模式?
-
Redis Sentinel :体量较小时,选择 Redis Sentinel ,单主 Redis 足以支撑业务。
-
Redis Cluster :Redis 官方提供的集群化方案,体量较大时,选择 Redis Cluster ,通过分片,使用更多内 存。
【15】是否使用过 Redis Cluster 集群,集群的原理是什么?
使用过 Redis 集群,它的原理是:
-
所有的节点相互连接
-
集群消息通信通过集群总线通信,集群总线端口大小为客户端服务端口 + 10000(固定值)
-
节点与节点之间通过二进制协议进行通信
-
客户端和集群节点之间通信和通常一样,通过文本协议进行
-
集群节点不会代理查询
-
数据按照 Slot 存储分布在多个 Redis 实例上
-
集群节点挂掉会自动故障转移
-
可以相对平滑扩/缩容节点
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0~16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
【16】假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?
我们可以使用 keys 命令和 scan 命令,但是会发现使用 scan 更好。
【17】如何解决缓存穿透问题?
答:查询一个redis和mysql肯定都不存在的数据是缓存穿透,例如查询id为-1的数据,多半为认为恶意攻击。解决方案:去数据库查不存在在redis存null,并且设置过期时间5分钟。
或者用布隆过器解决:布隆过滤器是一个bitMap数组,它说不存在的元素一定不存在,他说存在的未必存在。
【18】如何缓存击穿问题?
答:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。从造成问题的原因去解决,当热点Key失效的一刹那,大并发裸奔访问数据库,数据库被打垮。我们只需要在热点Key失效的一刹那保证只有一个请求过来就可以了。这种需求肯定用锁解决,理论上可以用同步锁解决,但这个不靠谱,锁只能锁一个进程,但是微服务是多个服务是多个进程,根本不起作用。所以用分布式锁解决。
【19】如何解决缓存雪崩问题?
答:大量缓存同时失效,数据库被击垮问题。解决思路不让缓存同时失效,比如加给缓存过期时间加随机数,但是当缓存数据足够大时,这个效果不那么明显了,可以通过二级缓存技术实现。
【20】缓存和数据库谁先更新呢?
先更新数据库,再延时删缓存。因为操作数据库的时间比操作缓存的时间长,先更新数据库,在延时删除缓存可以更好的保证缓存数据库和数据库数据一致。
但从实际应用角度来说,可以考虑使用cannal同步。
定时任务篇
【1】简述一下什么是任务调度?
答:任务调度就是按照特定时间规则执行系统某个固定的业务逻辑。任务调度底层是使用jdk的Timer实现的。单体项目建议使用Spring-task任务调度技术,分布式架构建议使用quartz任务调度框架。Spring-task是单线程运行旳,Quartz是多线程运行的,且功能更为丰富,支持作业管理。
【2】说一下你都用过什么任务调度技术,他们的区别是什么?
答:Spring-task是单线程,且功能简单。执行任务只需开启开关@EnableScheduling,在要执行的任务方法上加
@Scheduled(cron = "*/1 * * * * *")注解。它的使用弊端:
-
任务A的执行时间会影响任务B的执行间隔,但是任务A和任务B是两个任务,不应该相互影响。
-
没有固定组件,持久化等功能,也就没法形成作业系统
Quartz是多线程的高可用的任务调度框架,支持持久化,多线程,集群模式,且有固定组件结构Job、Trigger、scheduler。他的优点一一说明
-
有固定组件,有持久化功能,这样就能基于Quartz开发一个任务调度系统,通过UI界面去管理任务调度。
-
任务进行持久化之后,重启服务器会加载持久化的任务继续执行。
-
任务支持集群模式,如果任务调度模块是一个集群n个节点,那么任务调度不会因为一个节点挂掉而挂掉,且任务在集群之间形成负载均衡。
【3】补充,如果问代码如何实现?
需要注意,java中有很多技术很重要,但是在学习过程中由于学的东西太多,每项技术的学习时间很短,我们不可能时时刻刻都记住。这时可以说忘了,然后介绍一下大概组件就行了(这个就要提前准备了),在继续说查看一下自己做的笔记不影响开发。
分布式锁
【1】什么是分布式锁,你都了解哪些分布式锁
说到分布式锁,先说互斥锁,互斥锁解决多线程操作共享数据有可能产生线程安全而出现的互斥锁。简单来说就是一个线程操作共享数据库的时候,必须先获取锁才能操作,且其他线程在这个时间段不能获取锁,也不能操作共享数据,必须等待之前的线程释放锁再来获取锁再来操作共享数据。
如果是多个进程操作共享数据同样会产生共享数据安全问题,但是线程内的锁肯定失效了。所以必须借助外力实现分布式锁解决多进程操作共享数据问题。
可以借助mysql、redis、zookeeper实现分布式锁。
mysql:
基于分布式锁的实现,首先肯定是想单独分离出一台mysql数据库,所有服务要想操作文件(共享资源),那么必须先在mysql数据库中插入一个标志,插入标志的服务就持有了锁,并对文件进行操作,操作完成后,主动删除标志进行锁释放,其与服务会一直查询数据库,看是否标志有被占用,直到没有标志占用时自己才能写入标志获取锁。
但是这样有这么一个问题,如果服务(jvm1)宕机或者卡顿了,会一直持有锁未释放,这样就造成了死锁,因此就需要有一个监视锁进程时刻监视锁的状态,如果超过一定时间未释放就要进行主动清理锁标记,然后供其与服务继续获取锁。
如果监视锁字段进程和jvm1同时挂掉,依旧不能解决死锁问题,于是又增加一个监视锁字段进程,这样一个进程挂掉,还有另一个监视锁字段进程可以对锁进行管理。这样又诞生一个新的问题,两个监视进程必须进行同步,否则对于过期的情况管理存在不一致问题。
最终发现用mysql实现分布锁即繁琐又性能又低,因此我们很少用到。
当然还可以用mysql的行锁来实现分布锁,这要求多进程操作的共享数据在同一张表内,然后操作共享数据用一条update语句实现,操作共享数据条件也必须写在这条update语句中where条件中,因为update语句本身是行锁,同一时间只有一个update成功,也就解决分布锁问题。
补充 where条件必须有一个字段用了唯一索引,否则行锁失效,分布锁也就失效了。
redis:
(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。
zookeeper分布锁
-
创建一个目录mylock;
-
线程A想获取锁就在mylock目录下创建临时顺序节点;
-
获取mylock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
-
线程B获取所有节点,判断自己不是最小节点,设置监听比自己小的节点;
-
线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。
总结:
从性能角度(从高到低)
-
缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)
-
Zookeeper > 缓存 > 数据库
接着问,用不用介绍代码怎么实现呢?
@SpringBootTest
public class Demo {
@Autowired
private RedissonClient redissonClient;
@Test
public void test(){
//创建锁
RLock rLock = redissonClient.getLock("order:lock");
//获得锁
rLock.lock();
//释放锁
rLock.unlock();
}
}
//zookeeper 分布式锁解决超卖问题
//1.创建zookeeper连接
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client= CuratorFrameworkFactory.newClient("192.168.184.200:2181", retryPolicy);
client.start();
//创建分布式锁
InterProcessMutex interProcessMutex = new InterProcessMutex(client,"/ordersettinglock");
//加锁
try {
interProcessMutex.acquire();
Goods goods = this.baseMapper.selectById(id);
Integer count = goods.getCount();
if(count>0){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
this.baseMapper.subCount(id);
}
} catch (Exception e) {
throw new RuntimeException("超卖了");
} finally {
//释放锁
try {
interProcessMutex.release();
} catch (Exception e) {
throw new RuntimeException(e);
}
}