文章目录
- 4.1 NoSql
- 4.2 Redis
- 4.2.1 概念介绍
- 4.2.2 与其他 key - value 缓存产品相比的特点
- 4.2.3 优势
- 4.2.4 应用场景
- 4.2.5 常用指令
- 4.2.6 第三方工具(redis-desktop-manager)访问Redis
- 4.2.7 Redis数据结构
- 4.2.8 Redis常用指令
- 4.2.9 Redis的多数据库
- 4.2.10 Redis的事务管理
- 4.2.11 Redis发布订阅模式
- 4.2.12 Jedis连接Redis
- 4.2.13 Redis的持久化方式
- 4.2.14 Redis的主从复制
- 4.2.15 哨兵模式
- 4.2.16 Redis集群
- 4.2.17 Redis高端面试-缓存雪崩,缓存穿透,缓存击穿
- 4.2.18 Redis高端面试-分布式锁
4.1 NoSql
4.1.1 概念介绍
NoSQL:非关系型数据库;NoSQL数据库的产生就是为了解决大规模数据集合,多重数据种类带来的挑战,尤其是大数据应用难题。
4.1.2 产品区别
4.2 Redis
4.2.1 概念介绍
Redis:Remote Dictionary Server(远程字典服务器),是完全开源免费的,用C语言编写的,遵守BCD协议。是一个高性能的(key/value)分布式内存数据库,基于内存运行并支持持久化的NoSQL数据库,是当前最热门的NoSql数据库之一。
4.2.2 与其他 key - value 缓存产品相比的特点
- Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用
- Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
- Redis支持数据的备份,即master-slave(主从)模式的数据备份
4.2.3 优势
- 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
- 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
- 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
- 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 使用多路I/O复用模型,非阻塞IO;
4.2.4 应用场景
- 缓存(数据查询,短连接,新闻内容,商品内容等),使用最多
- 聊天室在线好友列表
- 任务队列(秒杀,抢购,12306等)
- 应用排行榜
- 网站访问统计
- 数据过期处理(可以精确到毫秒)
- 分布式集群架构中的session问题
4.2.5 常用指令
4.2.5.1 启动
- 前端模式启动(进入安装目录的bin目录)
直接运行bin/redis-server将使永前端模式启动,前端模式启动的缺点是启动完成后,不能再进行其他操作,如果 要操作必须使用ctrl+c,同时redis-server程序结束,不推荐此方法。
./redis-server
下面是启动界面(该界面只能启动,不能进行其他操作)
- 后端启动
修改配置文件,设置:daemonize yes。
启动时,指定配置文件(这里所在文件夹是redis)
4.2.5.2 查看是否启动
法一:
ps -ef | grep redis
,出现下图界面说明成功(ip地址已修改,原始是127.0.0.1)
法二 :
lsof -i:6379
,6379是redis的默认端口号
4.2.5.3 客户端访问redis
- 在bin文件夹下运行redis-cli,
./redis-cli
如果已修改过ip地址将出现下图的情况
- 连接指定的ip地址(Redis的地址)以及端口号
2.1 关闭防火墙
2.2 修改redis.conf中bind ip地址为bind 虚拟机ip地址
2.3 连接客户端:redis-cli -h ip地址 -p 端口号
(端口号默认为6379)
4.2.5.4 向Redis服务器发送命令
4.2.5.5 退出客户端
4.2.5.6 停止
法一:强制结束程序。强制终止Redis进程可能会导致redis持久化数据丢失。
kill -i pid
:pid通过ps -ef |grep redis
查询,每次都不同
法二:进入redis的bin目录,通过指令停止(访问和停止都在该目录下,启动在redis目录)
4.2.6 第三方工具(redis-desktop-manager)访问Redis
- 关闭防火墙
systemctl stop firewalld
- 修改redis.conf参数
- 配置连接
4.2.7 Redis数据结构
六种数据类型:字符串(String)、列表(List)、集合(set)、哈希结构(hash)、有序集合(zset)和基数(HyperLogLog)
4.2.8 Redis常用指令
4.2.8.1 String类型
赋值:
set key value
取值:get key
赋多个值:mset k1 value,k2 value..
取多个值:mget k1 k2
删除值:del key
4.2.8.2 字符串数字的递增与递减
递增数字:当存储的字符串是整数时,Redis提供了一个实用的命令INCR,其作用是让当前键值递增,并返回递增 后的值。
递增数字:incr key
递减数字:decr key
增加指定的整数:incrby key increment
减少指定的整数decrby key decrement
4.2.8.3 Hash散列
- hash叫散列类型,它提供了字段和字段值的映射,相当于是对象格式的存储
赋值:hset key field value
取值:hget key field
赋多个值:hmset key f1 v1 f2 v2 ..
取多个值:hmget key f1 f2
取所有字段值:hgetall key
删除字段:hdel key f1(f2...)
4.2.8.4 队列
列表左侧添加元素:
lpush key value
列表左侧弹出元素:lpop key
(临时存储,弹出后,从队列中清除)
列表右侧添加元素:rpush key value
列表右侧弹出元素::rpop key
获取列表中元素的个数:llen key
查看列表:lrange key start stop
返回start、stop之间的所有元素(包含两端)
4.2.8.5 Set集合
- Set集合无序,不重复
增加元素:sadd key member(memeber...)
删除元素:srem key member
获取集合所有元素:smembers key
判断元素是否在集合中:sismember key member
4.2.8.6 Zset集合
- Sortedset又叫zset,是有序集合,可排序的,但是唯一。Sortedset和set的不同之处,是会给set中的元素添加一个分数,然后通过这个分数进行排序。
增加元素:zadd key score member(score member...)
若该元素已存在则会用新的分数替换原有的分数
获得排名在某个范围的元素列表,并按照元素分数降序返回:zrevrange key start stop
获得排名在某个范围的元素列表(含分数):zrevrange key start stop withscores
获得元素分数:zscore key member
删除元素:zrem key member(member..)
给某一个属性加分数或减分,减分时使用负数:zincrby key score member
4.2.8.7 HyoperLogLog命令(了解)
- HyperLogLog是一种使用随机化的算法,以少量内存提供集合中唯一元素数量的近似值
- 基数:集合中不同元素的数量。比如 {‘apple’, ‘banana’, ‘cherry’, ‘banana’, ‘apple’} 的基数就是 3 。
- 估算值:算法给出的基数并不是精确的,可能会比实际稍微多一些或者稍微少一些,但会控制在合理的范围 之内
优点:即使输入元素的数量或者体积非常非常大,计算基数所需的空间总是固定的、并且是很 小的,即数据量越多该特性越容易体现。在Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数
将指定的元素添加到指定的HyperLogLog中:
pfadd key element(element..)
返回给定HyperLogLog的基数估算值:pfcount key(key..)
4.2.8.8 其他命令
- keys返回满足给定pattern的所有key:
keys user*
:返回user开头的所有key
key *
:返回所有key- exists确认一个key是否存在,存在返回1:
exists key
- 删除一个键:
del key
- 重命名一个键:
rename oldkey newkey
- type返回值的类型:
type key
- 设置key的生存时间:
expire key seconds
- 查看key剩余生存时间:
ttl key
- 清除生存时间:
persist key
- 获取服务器信息和统计:
info
- 删除当前数据库的所有键:
flushdb
(慎用!!!)- 删除所有数据库的所有键:flushall
4.2.9 Redis的多数据库
- 一个redis实例key包括多个数据库,客户端可以指定连接某个redis实例的哪个数据库,就好比一个mysql中创建多个数据库,客户端连接时指定连接哪个数据库。
- 一个redis实例最多可提供16个数据库,下标从0-15,客户端默认连接第0号数据库,也可以通过select选择连接哪个数据库.
选择数据库:select index
移动数据:move key index
4.2.10 Redis的事务管理
一个事务从开始到执行会经历以下三个阶段: 开始事务、命令入队、执行事务。
以下是一个事务的例子, 它先以 MULTI 开始一个事务, 然后将多个命令入队到事务中, 最后由 EXEC 命令触发事 务, 一并执行事务中的所有命令:
4.2.11 Redis发布订阅模式
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 Redis 客户端可以订阅任意数量的频道。下图展示了频道channel1,以及订阅这个频道的三个客户端——client2、client5和client1之间的关系:
- 首先创建频道名为:redisMessage
- 订阅端会显示如下信息
3.发布端开始发布内容(创建一个新的session)
- 订阅端显示内容
4.2.12 Jedis连接Redis
- 导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.2</version>
</dependency>
- 确认本地可以ping通vm
- 确认vm防火墙已关闭
(1) 单实例连接
public static void main(String[] args) {
Jedis jedis = new Jedis("10.39.18.223", 6379);
jedis.set("book1", "hh");
String book1 = jedis.get("book1");
System.out.println("book1的值:"+book1);
}
- 常见异常
- 解决方案
(2)连接池连接
public static void main(String[] args) {
//1. 创建连接池配置的工具类对象
JedisPoolConfig jedisPoolConfig=new JedisPoolConfig();
JedisPool jedisPool=null;
Jedis resource=null;
jedisPoolConfig.setMaxIdle(10);//最大空闲数
jedisPoolConfig.setMaxTotal(20);//总的连接数
try {
//2. 创建连接池对象
jedisPool = new JedisPool(jedisPoolConfig, "10.39.18.223", 6379);
//3. 获得jedis资源
resource = jedisPool.getResource();
//4. 操作数据
resource.set("u2", "xixi");
String u2 = resource.get("u2");
System.out.println("u2的值:" + u2);
} catch (Exception e) {
e.printStackTrace();
} finally {
//5. 关闭资源
if (resource != null) {
resource.close();
}
if (jedisPool != null) {
jedisPool.close();
}
}
}
4.2.13 Redis的持久化方式
4.2.13.1 概念介绍
由于redis的值放在内存中,为防止突然断电等特殊情况的发生,需要对数据进行持久化备份。即将内存数据保存到硬盘。
4.2.13.2 Redis持久化存储方式
- RDB持久化
RDB 是以二进制文件,是在某个时间点将数据写入一个临时文件,持久化结束后,用这个临时文件替换上次持久化 的文件,达到数据恢复。
优点:使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 redis 的高性能
缺点:RDB 是间隔一段时间进行持久化,如果持久化之间redis发生故障,会发生数据丢失。所以这种方式更适合 数据要求不严谨的时候。
这里说的这个执行数据写入到临时文件的时间点是可以通过配置来自己确定的,通过配置redis 在 n 秒内如果超过 m 个 key 被修改这执行一次 RDB操作。这个操作就类似于在这个时间点来保存一次 Redis 的所有数据,一次快照数据。所有这个持久化方法也通常叫做 snapshots。
RDB 默认开启,redis.conf 中的具体配置参数如下;(测试时使用root用户操作)
- AOF持久化
Append-Only File,将“操作 + 数据”以格式化指令的方式追加到操作日志文件的尾部,在 append 操作返回后(已经 写入到文件或者将要写入),才进行实际的数据变更,“日志文件”保存了历史所有的操作过程;当 server 需要数据恢复时,可以直接replay此日志文件,即可还原所有的操作过程。AOF 相对可靠,AOF 文件内容是字符串,非常 容易阅读和解析。
优点:可以保持更高的数据完整性,如果设置追加file的时间是1s,如果redis发生故障,最多会丢失1s的数据;
缺点:AOF 文件比 RDB 文件大,且恢复速度慢。
AOF 默认关闭,开启方法,修改配置文件 reds.conf:appendonly yes
4.2.14 Redis的主从复制
4.2.14.1 概念介绍
- 持久化保证了即使redis服务重启也不会丢失数据,但是当redis服务器的硬盘损坏了可能会导致数据丢失,通过redis的主从复制机制就可以避免这种单点故障(单台服务器的故障)。
- 主redis中的数据和从redis的数据保持实时同步,当主redis写入数据时通过主从复制机制复制到两个从服务上。
- 主从复制不会阻塞master,在同步数据时,master 可以继续处理client 请求.
- 主机master配置:无需配置
工作中一般选用:一主两从或一主一从
4.2.14.2 主从搭建步骤
- 复制一个从机,使用root用户
- 修改从机redis.conf配置
- 语法:replicaof // replicaof 主机ip 主机端口号
- 检索文件:输入/replicaof 当前页没有时,输入n,查找下一页
3. 修改从机端口为6380
4. 清除从机持久化文件
5. 启动从机
6. 启动6380的客户端
注意:
- 主机一旦发生增删改操作,那么从机会自动将数据同步到从机中
- 从机不能执行写操作,只能读
复制的过程原理
- 当从库和主库建立MS(master slaver)关系后,从库会向主数据库发送SYNC命令;
- 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来;
- 快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis;
- 从Redis接收到后,会载入快照文件并且执行收到的缓存命令;
- 主Redis 每当接收到写命令时就会将命令发送从Redis,保证数据的一致;【内部完成,所以不支持客户端在从机人为写数据。】
复制架构中出现宕机情况
- 手动执行
- 从Redis宕机:重启
- 主Redis宕机:从机执行SLAVEOF NO ONE命令,断开主从关系并且提升为主库继续服务[这个时候新主机(之前的从机)就具备写入的能力];
主服务器修好后,重新启动后,执行SLAVEOF命令,将其 设置为从库[老主机设置为从机]。
- 哨兵模式
4.2.15 哨兵模式
4.2.15.1 概念介绍
给集群分配一个站岗的。对Redis系统的运行情况监控,它是一个独立进程。
4.2.15.2 功能
- 监控主数据库和从数据库是否运行正常;
- 主数据出现故障后自动将从数据库转化为主数据库;
4.2.15.2 环境准备
一主两从,启动任一从机时,启动哨兵模式
虽然哨兵(sentinel) 释出为一个单独的可执行文件 redis-sentinel ,但实际上它只是一个运行在特殊模式下的Redis服务器,你可以在启动一个普通Redis服务器时通过给定 --sentinel 选项来启动哨兵(sentinel)。
4.2.15.3 配置流程
- 生成两台从机,清除里面的持久化文件
- 启动一主两从
- 查看配置信息:
info replication
- 配置哨兵
- 在任意一台从redis的bin目录下创建文件sentinel.conf并输入
:sentinel monitor mastername ip地址 6379 1
。- 说明:
- mastername 主机的名称,自定义
- ip地址:主机的IP;
- 6379:端口
- 1:最低通过票数
- 再次启动从机(配置了哨兵的),并以日志形式展示
启动redis服务后,程序会自动配置文件sentinel.conf,并生成内容。
-
启动哨兵模式
-
查看进程
-
模拟主机宕机
-
杀死主机进程
-
查看从机信息(配置了哨兵的从机不一定成为主机)
4.2.16 Redis集群
4.2.16.1 概念介绍
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
- 节点的fail是通过集群中超过半数的节点检测有效时整个集群才生效.
- 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可.
- redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster负责维护node<->slot<->value.
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽, redis 会根据节点数量大致均等的将哈希槽映射到不同的节点
4.2.16.2 心跳机制
- 集群中所有master参与投票,如果半数以上master节点与其中一个master节点通信超过(cluster-node-timeout), 认为该master节点挂掉.
- 什么时候整个集群不可用(cluster_state:fail)?
- 如果集群任意master挂掉,且当前master没有slave,则集群进入fail状态。也可以理解成集群的[0-16383]slot映射不完全时进入fail状态。
- 如果集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态
4.2.16.3 集群搭建步骤
-
创建集群目录
-
集群目录下创建节点目录
- 搭建集群最少也得需要3台主机,如果每台主机再配置一台从机的话,则最少需要6台机器。 设计端口如下:创建6 个redis实例,需要端口号7001~7006
-
如果存在持久化文件,则删除
-
修改redis.conf配置文件,打开
Cluster-enable yes
-
修改端口
-
复制剩余5台机器(7002-7006)
-
启动6台机器,通过自定义shell脚本
-
修改startall.sh文件权限
-
启动所有的实例
-
创建集群(关闭防火墙)
在任意一台上运行 不要在每台机器上都运行,一台就够了
11. 连接集群
12. 查看集群信息
- 查看集群节点信息
4.2.16.4 Jedis访问集群
注意:如果redis重启,需要将redis中生成的dump.rdb和nodes.conf文件删除,然后再重启。
- 添加依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
- 核心代码
public static void main(String[] args) throws IOException {
// 创建一连接,JedisCluster对象,在系统中是单例存在
Set<HostAndPort> nodes = new HashSet<HostAndPort>();
nodes.add(new HostAndPort("192.168.197.132", 7001));
nodes.add(new HostAndPort("192.168.197.132", 7002));
nodes.add(new HostAndPort("192.168.197.132", 7003));
nodes.add(new HostAndPort("192.168.197.132", 7004));
nodes.add(new HostAndPort("192.168.197.132", 7005));
nodes.add(new HostAndPort("192.168.197.132", 7006));
JedisCluster cluster = new JedisCluster(nodes);
// 执行JedisCluster对象中的方法,方法和redis指令一一对应。
cluster.set("test1", "test111");
String result = cluster.get("test1");
System.out.println(result);
//存储List数据到列表中
cluster.lpush("site-list", "java");
cluster.lpush("site-list", "c");
cluster.lpush("site-list", "mysql");
// 获取存储的数据并输出
List<String> list = cluster.lrange("site-list", 0 ,2);
for(int i=0; i<list.size(); i++) {
System.out.println("列表项为: "+list.get(i)); }
// 程序结束时需要关闭JedisCluster对象
cluster.close();
System.out.println("集群测试成功!");
}
4.2.17 Redis高端面试-缓存雪崩,缓存穿透,缓存击穿
4.2.17.1 缓存的概念
- 广义:
在第一次加载某些可能会复用数据的时候,在加载数据的同时,将数据放到一个指定的地点做保 存。再下次加载的时候,从这个指定地点去取数据。这里加缓存是有一个前提的,就是从这个地方取数据,比从数 据源取数据要快的多。- java狭义缓存:
- 虚拟机缓存(ehcache,JBoss Cache)
- 分布式缓存(redis,memcache)
- 数据库缓存
正常来说,速度由上到下依次减慢
4.2.17.2 缓存雪崩
- 产生原因
由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从 Redis中获取,如下图)所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力, 严重的会造成数据库宕机,造成系统的崩溃。
缓存失效如下图
- 解决方案
(1)在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据 和写缓存,其他线程等待。虽然能够在一定的程度上缓解了数据库的压力但是与此同时又降低了系统的吞吐量。同样会导致用户等待超时,这是个治标不治本的方法。
(2)分析用户的行为,不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。(推荐)
4.2.17.3 缓存穿透
- 产生原因
用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找 不到,每次都要去数据库再查询一遍,然后返回空。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
- 解决方案
(1) 设置一个默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。
(2) 把空结果,也给缓存起来,这样下次同样的请求就可以直接返回空了,既可以避免当查询的值为空时引起的缓存 穿透。同时也可以单独设置个缓存区域存储空值,对要查询的key进行预先校验,然后再放行给后面的正常缓存处 理逻辑。
注意:再给对应的ip存放真值的时候,需要先清除对应的之前的空缓存。
4.2.17.4 缓存击穿
- 产生原因
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。 这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。(类似微博热搜)某个key访问非常频繁,当key失效的时候有大量线程来构建缓存,导致负载增加,系统崩溃。
- 解决方案
(1) 使用锁,单机用synchronized,lock等,分布式用分布式锁。
(2) 缓存过期时间不设置,而是设置在key对应的value里。如果检测到存的时间超过过期时间则异步更新缓存。
4.2.18 Redis高端面试-分布式锁
4.2.18.1 使用分布式锁条件
- 系统是一个分布式系统(单机的可以使用ReentrantLock或者synchronized代码块来实现)
- 共享资源(各个系统访问同一个资源,资源的载体可能是传统关系型数据库或者NoSQL)
- 同步访问(即有很多个进程同时访问同一个共享资源。)
4.2.18.2 分布式锁概念介绍
- 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码 段。线程锁只在同一JVM中有效果。
- 进程锁:为了控制同一操作系统中多个进程访问某个共享资源。
- 分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
4.2.18.3 使用redis的setNX命令实现分布式锁
- 实现原理
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争 关系。redis的SETNX命令可以方便的实现分布式锁
- 基本命令解析
(1)setNX(SET if Not eXists)
语法:
SETNX key value
将 key的值设为 value ,当且仅当key不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。
返回值:1:成功,0:失败
所以我们使用执行下面的命令SETNX可以用作加锁原语(locking primitive)。比如说,要对关键字(key) foo 加锁,客户端可以尝试以下方式:
- 如果 SETNX返回 1 ,说明客户端已经获得锁,SETNX将键lock.foo的值设置为锁的超时时间(当前时间 + 锁的有效时间)。 之后客户端可以通过
DEL lock.foo
来释放锁。- 如果 SETNX返回 0 ,说明 key 已经被其他客户端上锁。如果锁是非阻塞(non blocking lock)的,我们可以选择返回调用,或者进入一个重试循环,直到成功获得锁或重试超时(timeout)。
(2)getSET
先获取key对应的value值。若不存在则返回nil,然后将旧的value更新为新的value
(如果发现别的客户端的锁超时了,而其自身无法释放锁,通过这种方式比直接del lock.foo
释放锁更友好,毕竟哪个客户端上锁,就应该由哪个客户端来解锁)
语法:GETSET key value
将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 当 key 存在但不是字符串类型时,返回一个错误。
返回值:key存在时,返回旧值,key无旧值时(key不存在时),返回nil
- 注意的关键点:(回答面试的核心点)
- 同一时刻只能有一个进程获取到锁。setnx
- 释放锁:锁信息必须是会过期超时的,不能让一个线程长期占有一个锁而导致死锁; (最简单的方式就是del, 如果在删除之前死锁了。)
解决死锁
上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?
我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁 已失效,可以被重新使用。
C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。 C1 发送DEL lock.foo C1 发送SETNX lock.foo 并且成功了。 C2 发送DEL lock.foo C2 发送SETNX lock.foo 并且成功了。 这样一来,C1,C2 都拿到了锁!问题大了!幸好这种问题是可以避免的,让我们来看看C3这个客户端是怎样做的:
C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0 C3发送GET lock.foo 以检查锁 是否超时了,
如果没超时,则等待或重试。
如果已超时,C3通过下面的操作来尝试获得锁:GETSET lock.foo通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的 影响可以忽略不计。
注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时, 再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就 不必解锁了。