# Redis从入门到放弃

# 基础操作

# 查询各个db对应的key数

info keyspace

# 查询连接数

info clients

# 查询和设置最大连接数

查询最大连接数
config get maxclients
设置~
config set maxclients 10

# 关闭redis

关闭本地redis
./redis-cli shutdown
使用客户端远程关闭
redis-cli -h xxx.xxx.xx.xx -p 6379 shutdown

# redis设置密码和redis-cli登陆验证auth

1.查看密码

config get requirepass

2.设置密码

临时设置

config set requirepass 123456

配置文件设置

requirepass 123456

3.使用redis-cli登陆后验证密码

auth 123456

# scan高效遍历数据

有时候需要使用keys匹配返回所有相关的数据做统计, 其实redis本身就不适合做此类工作, 但如果硬是有这个相关需求, 需要注意redis中存放的数据量基数, 否则会出现阻塞, 导致线上事故.

为什么不推荐用Keys

  1. 如果匹配的数据量大, 很难一次性接收或显示
  2. keys使用遍历算法, 复杂度是O(n)
  3. redis是单线程, 当前命令会阻塞其他命令执行

推荐使用scan命令

2.8版本开始提供 scan复杂度为O(n), 通过游标分布进行, 不阻塞线程. 并且提供limit控制返回结果的槽数, 因此返回结果数量多少不固定. 返回结果可能出现重复, 要注意去重. 遍历过程中修改的数据, 可能会出现遍历不到的情况

如何使用

SCAN相关命令包括SSCAN 命令、HSCAN 命令和 ZSCAN 命令 命令格式: SCAN cursor [MATCH pattern] [COUNT count]

示例
查询当前db从cursor0开始的一百的slot的key
scan 0 match * count 100

参考

Redis 为什么不建议使用 keys 命令 (opens new window)

如何统计redis key数量 (opens new window)

# 发布与订阅(pub/sub)

通过建立一个channel,让发布者与订阅者进行通信。

# 压测Redis

常见的方式是使用redis-benchmark,以下介绍其使用方法。

redis-benchmark使用说明如下:

Usage: redis-benchmark [-h] [-p] [-c] [-n[-k]
 -h     Server hostname (default 127.0.0.1)
 -p     Server port (default 6379)
 -s     Server socket (overrides host and port)
 -c     Number of parallel connections (default 50)
 -n     Total number of requests (default 10000)
 -d      Data size of SET/GET value in bytes (default 2)
 -k      1=keep alive 0=reconnect (default 1)
 -r      Use random keys for SET/GET/INCR, random values for SADD
  Using this option the benchmark will get/set keys
  in the form mykey_rand:000000012456 instead of constant
  keys, the argument determines the max
  number of values for the random number. For instance
  if set to 10 only rand:000000000000 - rand:000000000009
  range will be allowed.
 -P       Pipelinerequests. Default 1 (no pipeline).
 -q       Quiet. Just show query/sec values
 —csv     Output in CSV format
 -l       Loop. Run the tests forever
 -t       Only run the comma-separated list of tests. The test
  names are the same as the ones produced as output.
 -I       Idle mode. Just open N idle connections and wait.

中文说明:

序号 选项 描述 默认值
1 -h 指定服务器主机名 127.0.0.1
2 -p 指定服务器端口 6379
3 -s 指定服务器 socket
4 -c 指定并发连接数 50
5 -n 指定请求数 10000
6 -d 以字节的形式指定 SET/GET 值的数据大小 2
7 -k 1=keep alive 0=reconnect 1
8 -r SET/GET/INCR 使用随机 key, SADD 使用随机值
9 -P 通过管道传输 请求 1
10 -q 强制退出 redis。仅显示 query/sec 值
11 --csv 以 CSV 格式输出
12 -l 生成循环,永久执行测试
13 -t 仅运行以逗号分隔的测试命令列表。
14 -I Idle 模式。仅打开 N 个 idle 连接并等待。

测试命令示例:

  1. redis-benchmark -h 192.168.1.201 -p 6379 -c 100 -n 100000
    

    100个并发连接,100000个请求,检测host为localhost端口为6379的redis服务器性能。

  2. redis-benchmark -h 192.168.1.201 -p 6379 -q -d 100
    

    测试存取大小为100字节的数据包的性能。

  3. redis-benchmark -t set,lpush -n 100000 -q
    

    只测试某些操作的性能。

  4. redis-benchmark -n 100000 -q script load "redis.call(‘set’,’foo’,’bar’)"
    

    只测试某些数值存取的性能。

常加上参数-q来quiet执行命令,显示简洁的结果。

# Redis数据类型

# String

常规的set/get操作, 可存放String或者数字, 一般用于复杂的计数功能缓存.

# Hash

结构化对象, 容易操作其中的某个字段, 常用于解决单点登录.

# List

可以做简单的消息队列, 还可以利用Irange做基于redis的分页功能.

# Set

Set中存放的是不重复的集合, 可以用来做全局的去重功能. 通常是集群部署, 因此利用set进行交集/并集/差集等操作完成一些功能比Java的set方便.

# Sorted Set

多了一个权重参数Score, 集合中的元素能够按Score进行排列. 常用来做排行榜/取TopN/延时任务等操作.


# Redis安装

linux下安装redis

# wget http://download.redis.io/redis-stable.tar.gz
# tar xzf redis-6.0.8.tar.gz
# cd redis-6.0.8
# make

执行完make后,会出现redis-server和redis-cli(位于src下)。

启动redis:

# cd src
默认配置启动
# ./redis-server
指定配置启动
# ./redis-server ../redis.conf

# Redis配置

# 设置redis为后台服务/后台启动

将配置文件中的daemonize no改为daemonize yes

# 设置访问密码

去掉配置文件中requirepass的注释符,并在后面添加密码。

# Redis主从复制

# 主从复制主要作用

  • 多机热备
  • 读写分离
  • 负载均衡
  • 故障恢复

# 配置主从复制

主从复制是在从节点发起的,不需要主节点做任何事情。

  1. 修改从节点配置文件

    配置文件中加入salveof 127.0.0.1 6500

  2. 先启动主节点,再启动从节点

    启动从节点要指定执行对应修改的配置文件./redis-server ../redis2.conf

# Redis-cli使用

# 关闭本地redis
./redis-cli shutdown
# 使用客户端远程关闭
redis-cli -h xxx.xxx.xx.xx -p 6379 shutdown
# 连接远程redis
redis-cli -h xxx -p 6379

# Redis Sentinel 高可用

哨兵架构由两部分组成:数据节点、哨兵节点。

数据节点:普通的主从节点,并无区别

哨兵节点:特殊节点,不存储数据

# 部署哨兵节点

三个哨兵节点配置文件除了端口完全相同即可。

配置:

port 6700
daemonize yes
logfile sentinel-6700.log
sentinel monitor mymaster 127.0.0.1 6379 2

sentinel monitor mymaster 127.0.0.1 6379 2 表示哨兵监控127.0.0.1:6379主节点,该主节点名称是mymaster;2表示至少需要两个哨兵节点同意才能判断主节点故障进行故障转移

启动哨兵:

# 两种方式皆可启动哨兵,没区别,调用同一个脚本
redis-sentinel sentinel-6700.conf
redis-server sentinel-6700.conf --sentinel

使用redis-cli连接哨兵节点:

redis-cli -h 127.0.0.1 -p 6700

使用info查看哨兵信息:

127.0.0.1:6700> info Sentinel
# Sentinel
sentinel_masters:1  # 哨兵主节点1个
sentinel_tilt:0
sentinel_running_scripts:0
sentinel_scripts_queue_length:0
sentinel_simulate_failure_flags:0
master0:name=mymaster,status=ok,address=127.0.0.1:6379,slaves=2,sentinels=3 # 数据主节点地址,数据从节点个数,哨兵个数

此时查看哨兵节点配置文件,会发现有变化:

port 6700
daemonize yes
logfile "sentinel-6700.log"
sentinel monitor mymaster 127.0.0.1 6379 2
# Generated by CONFIG REWRITE
protected-mode no
pidfile "/var/run/redis.pid"
user default on nopass ~* &* +@all
dir "/home/jesse/redis"
sentinel myid 01f804d4c24a122e82183579cbbf2322a92a016e
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel current-epoch 0
sentinel known-replica mymaster 127.0.0.1 6500
sentinel known-replica mymaster 127.0.0.1 6501
sentinel known-sentinel mymaster 127.0.0.1 6701 3a010f567e4d3ad5c4d9f644c14299a3add0de89
sentinel known-sentinel mymaster 127.0.0.1 6702 6564664c93e086cf79842ba0911654280dec2141

从上面能看出数据从节点、哨兵从节点。

# 哨兵的故障转移

当redis数据主节点宕机(kill掉),可以连接哨兵查看info。一开始info没有变化,等一小会哨兵发现主节点故障, 执行故障转移,info中可以看到master节点address变为之前的数据从节点了:

image-20210721002843487

重启原来主节点,可以发现自动变为了从节点。

哨兵执行故障转移,会改写所有的配置文件。

哨兵可以配置监控多个主节点,配置多个sentinel monitor即可。

# Redis Cluster

集群由多个节点组成,数据分布在这些节点中。

集群节点分为主节点、从节点:只有主节点负责读写请求和维护集群信息,从节点只用来同步主节点数据和状态。

# 集群的作用

  • 数据分片
    • 突破单节点内存大小限制
    • 每个主节点都可以对外提供读写服务,提高了响应能力
  • 高可用
    • 主从复制
    • 主节点故障自动转移

# 启动集群

通常使用脚本来启动,但也可以按照下面步骤手动启动

配置集群节点:

#redis-7000.conf
port 7000
cluster-enabled yes
cluster-config-file "node-7000.conf"
logfile "log-7000.log"
dbfilename "dump-7000.rdb"
daemonize yes

cluster-enabled 表示开启集群模式

cluster-config-file 指定集群配置文件的位置。每个集群节点会维护一份集群配置文件,当集群信息发生变化时,节点会更新到配置文件中。当节点启动时,先会读取集群配置文件,如果没有配置文件,则初始化一份集群配置文件。

执行cluster nodes命令查看集群节点情况:

$ src/redis-cli -p 7000 cluster nodes
9817df9ac014be0e3325e1b03047e778782db501 :7000@17000 myself,master - 0 0 0 connected

第一列代表节点id,重启不会重新生成。

节点握手

以同样方式启动多个集群节点,再将它们进行节点握手,组成一个网络。

在7000节点中执行CLUSTER MEET 192.168.3.29 7001,即可完成7000节点与7001节点的握手。握手后可再查看集群节点情况。

~/redis$ src/redis-cli -p 7000
127.0.0.1:7000> CLUSTER MEET 192.168.3.29 7001
OK
127.0.0.1:7000> CLUSTER MEET 192.168.3.29 7002
OK
127.0.0.1:7000> CLUSTER MEET 192.168.3.29 7003
OK
127.0.0.1:7000> 
~/redis$ src/redis-cli -p 7000 cluster nodes
9817df9ac014be0e3325e1b03047e778782db501 192.168.3.29:7000@17000 myself,master - 0 1626882680000 2 connected
d5330393fabe6cabcfa9a490c3f334cf7ed8c2ee 192.168.3.29:7002@17002 master - 0 1626882681206 0 connected
78a2919c8952e9db4b84fc04439a5b9d5dc869e3 192.168.3.29:7001@17001 master - 0 1626882680201 1 connected
0ddfec297e09e3428801ee1106876fdc19e07886 192.168.3.29:7003@17003 handshake - 0 0 0 disconnected

7003是没有启动redis集群节点的,所以显示disconnected。

分配槽

集群一共有16384(0-16383)个槽,槽是数据管理和迁移的基本单位。将所有槽分配给集群节点之前,集群是下线状态,分配之后,集群将变为上线状态。

分配前可通过cluster info查看集群状态:

~/redis$ src/redis-cli -p 7000 cluster info
cluster_state:fail
cluster_slots_assigned:0
cluster_slots_ok:0
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:0
cluster_current_epoch:2
cluster_my_epoch:2
cluster_stats_messages_ping_sent:1522
cluster_stats_messages_pong_sent:1455
cluster_stats_messages_meet_sent:3
cluster_stats_messages_sent:2980
cluster_stats_messages_ping_received:1455
cluster_stats_messages_pong_received:1376
cluster_stats_messages_received:2831

集群状态显示fail:cluster_state:fail

使用cluster addslots命令分配槽:

~/redis$ src/redis-cli -p 7000 cluster addslots {0..5461}
OK
~/redis$ src/redis-cli -p 7001 cluster addslots {5462..10922}
OK
~/redis$ src/redis-cli -p 7002 cluster addslots {10923..16383}
OK

再查看集群状态:

~/redis$ src/redis-cli -p 7000 cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:3
cluster_size:3
cluster_current_epoch:2
cluster_my_epoch:2
cluster_stats_messages_ping_sent:1688
cluster_stats_messages_pong_sent:1632
cluster_stats_messages_meet_sent:3
cluster_stats_messages_sent:3323
cluster_stats_messages_ping_received:1632
cluster_stats_messages_pong_received:1542
cluster_stats_messages_received:3174

此时集群就可以对外提供服务了。

接下来完善集群的高可用:指定主从关系

通过cluster replicate为普通集群从节点指定主节点:

由于从节点没有执行握手,导致报错:

~/redis$ src/redis-cli -p 8000 cluster replicate 9817df9ac014be0e3325e1b03047e778782db501
(error) ERR Unknown node 9817df9ac014be0e3325e1b03047e778782db501

握手加入集群后,成功配置主从关系:

~/redis$ src/redis-cli -p 7000
127.0.0.1:7000> CLUSTER MEET 192.168.3.29 8000
OK
jesse@jesse-shenzhou:~/redis$ ^C
~/redis$ src/redis-cli -p 8000 cluster replicate 9817df9ac014be0e3325e1b03047e778782db501
OK
~/redis$ src/redis-cli -p 7000 cluster nodes
9817df9ac014be0e3325e1b03047e778782db501 192.168.3.29:7000@17000 myself,master - 0 1626885259000 2 connected 0-5461
d5330393fabe6cabcfa9a490c3f334cf7ed8c2ee 192.168.3.29:7002@17002 master - 0 1626885260792 0 connected 10923-16383
2056c500a97e1ad6b68f5fbd91f14da9310d66bd 192.168.3.29:8000@18000 slave 9817df9ac014be0e3325e1b03047e778782db501 0 1626885261796 2 connected
78a2919c8952e9db4b84fc04439a5b9d5dc869e3 192.168.3.29:7001@17001 master - 0 1626885260000 1 connected 5462-10922

可以看到8000节点是slave连接状态。

# 集群部署的方案设计

设计集群方案时,至少要考虑以下因素:

(1)高可用要求:根据故障转移的原理,至少需要3个主节点才能完成故障转移,且3个主节点不应在同一台物理机上;每个主节点至少需要1个从节点,且主从节点不应在一台物理机上;因此高可用集群至少包含6个节点。

(2)数据量和访问量:估算应用需要的数据量和总访问量(考虑业务发展,留有冗余),结合每个主节点的容量和能承受的访问量(可以通过benchmark得到较准确估计),计算需要的主节点数量。

(3)节点数量限制:Redis官方给出的节点数量限制为1000,主要是考虑节点间通信带来的消耗。在实际应用中应尽量避免大集群;如果节点数量不足以满足应用对Redis数据量和访问量的要求,可以考虑:(1)业务分割,大集群分为多个小集群;(2)减少不必要的数据;(3)调整数据过期策略等。

(4)适度冗余:Redis可以在不影响集群服务的情况下增加节点,因此节点数量适当冗余即可,不用太大。

# 集群原理

# Redis的过期策略和内存淘汰机制

比如你redis只能存5G数据, 可是写入了10G, 此时会删除5G数据, 请问redis是如何删除的?

Redis: 采用定期删除+惰性删除策略.

定期删除: redis默认每100ms随机抽取检查, 有过期的key则删除.

惰性删除: ???

内存淘汰机制: 在redis.conf中有如下一行配置:

maxmemory-policy volatile-lru

这是用于配置内存淘汰策略的:

前提: 当内存不足以写入新数据时.

noeviction: 新写入操作会报错.
allkeys-lru: 移除最近最少使用的key.
allkeys-random:
volatile-lru:
volatile-random:
volatile-ttl:

# 单线程的redis为什么这么快

  1. 纯内存操作
  2. 单线程操作, 避免了频繁的上下文切换
  3. 采用了非阻塞I/O多路复用机制

# 缓存穿透

即大量请求缓存中不存在的数据, 导致所有的请求都落到了数据库上, 从而数据库连接异常.

解决方案:

  1. 互斥锁 请自己思考! [^_^]: 缓存失效时, 先去获得锁, 得到锁后再去请求数据库, 没得到锁则休眠一段时间重试.

  2. 异步更新策略 请自己思考! [^_^]: 无论key是否取到值, 都直接返回, value中维护一个缓存失效时间, 缓存如果过期, 异步起一个线程去读数据库, 更新缓存

  3. 提供拦截机制判断请求是否有效. 请自己思考! [^_^]: 比如利用布隆过滤器, 内部维护一系列合法有效的key, 迅速判断请求的key是否合法有效, 不合法则直接返回.

# 缓存雪崩

即同一时间缓存大面积失效, 导致大量请求都落到了数据库上, 从而数据库连接异常.

解决方案:

  1. 让缓存避免同时失效
  2. 双缓存

具体解答请自己多思考 [^_^]: 1. 给缓存失效时间, 加上一个随机值, 避免同时失效. 2. 使用互斥锁(不推荐, 吞吐量明显下降) 3. 双缓存. A(失效时间20分钟) B(不设失效时间). 从A中读, 没有则读B, 并异步启动一个更新线程, 同时更新A和B.

# 如何解决redis并发竞争Key的问题

同时多个子系统去set一个key. 如果在非集群环境可以使用redis事务, 而集群环境下 做了数据分片操作, 一个事务中涉及到多个key操作的时候, redis事务显然不行.

  1. 如果对key的操作, 不要求顺序

可以使用分布式锁, 抢到了锁就去做set.

  1. 如果对key的操作, 要求顺序

设置时间戳解决或者利用队列将set方法变成串行访问.

将value带上时间戳,
有一个key, 系统A设置key为valueA, B设置key为valueB, C设置key为valueC.
期望key的value值按照valueA>valueB>valueC.
那么, B先抢到锁, 将key设置为valueB, 接下来A抢到了锁, 发现自己valueA的时间戳早于缓存中的时间戳, 就不操作set了, 以此类推.

# redis还应该消灭什么(TODO)

# redis复制原理和优化策略

# redis分布式解决方案

# redis和数据库双写一致性问题

# Redis的慢查询

由于redis被用来做高速缓存,因此对于其性能要求非常高,如果出现慢查询,对于"应用"中的使用可能是不可容忍的,同时慢查询的出现,也可能redis实例不稳定或宕机的预兆。

好在redis本身提供了慢查询功能,接下来介绍其如何使用。

# 需要了解的命令

  1. config
  2. slowlog

# config

用于获取&设置redis的相关配置参数,用法如下:

以上用法中get&set都很好理解,resetstat是指复位info的统计信息,rewrite是指将运行后set的配置持久化到redis.conf中。

# slowlog

此为redis提供的慢查询功能命令,下一节会做具体介绍。

  • slowlog get
  • slowlog len
  • slowlog reset

# 如何使用redis慢查询功能

先使用slowlog len获取慢查询的长度,然后通过slowlog get [序号]获取对应慢查询日志,经过一次排查或诊断后,通常需要slowlog reset将其重置,以便下次排查。

慢查询日志格式说明:

redis 127.0.0.1:6379> slowlog get 2
    1) 1) (integer) 14             //slowlog 唯一标识
       2) (integer) 1309448221     //unix 时间戳
       3) (integer) 15             //命令执行的时间,单位:微秒
       4) 1) "ping"                //具体执行的命令,最多记录128
    2) 1) (integer) 13
       2) (integer) 1309448128
       3) (integer) 30
       4) 1) "slowlog"
          2) "get"
          3) "100"

通常还需要注意慢查询的参数配置,有两个相关参数:

  • slowlog-log-slower-than
  • slowlog-max-len

slowlog-log-slower-than指redis操作超过指定时间(微秒),便会记录在慢查询中。

slowlog-max-len指慢查询功能最大记录条数。

# Redis面试问题汇总

redis面试题 (opens new window)

# Redisson

# Redis问题汇总

# Jedis、jedis的一个线上bug排查

jedis是一个连接redis的java客户端,并且在使用上提供连接池以提高性能。

本文jedis版本为:2.9.1(此版本存在缺陷,请更新为最新版或2.9.3)

# 应用出现假死

应用中使用jedis来连接redis,当出现并发时,突然应用中大部分业务出现假死的情况,通过查看jstack,发现所有线程都是卡在从jedisPool获取jedis连接上,随即开始深度排查。

# 检查jedisPool获取连接代码

首先简单检查了一遍卡住的相关源码,发现代码没有逻辑问题,阻塞在这里只可能是连接池里确实没有连接能获取。

# 猜想是否因为并发过高?

然后便猜想是不是因为并发过高,redis操作过慢,没有及时返还对应的连接导致大量获取连接的线程阻塞,但其实并没有问题,因为最近上线的版本确实会导致redis并发过高。(提出猜想时思考不是很缜密,因为从日志上)

# 检查redis连接池配置

检查配置文件的连接池,发现redis连接配置并未生效,应用中使用的仍是jedis默认配置,也就是8个连接数,此时先将配置问题进行修正,使其生效,发布上线。

但仔细想想仍然不对,就算是默认的8个连接数,应用运行几个月都没有出现过问题,为什么出现高并发场景就出事了?

# 推翻猜想

接着对应用并发情况进行评估,发现虽然间歇性会出现并发场景,但不可能会导致所有线程都卡住,而且从日志观察,应该没有线程能获取到jedis连接(由于还有业务没有用到redis,可以正常使用,因此单从量级很大的日志上分析筛选还不太肯定)。

# 检查使用jedis后是否close

如果通过jedis操作redis后,不进行close操作,那jedis连接不会返回到pool中,因此这种情况也可能会导致出现假死现象。

# 大部分都是通过redisTemplate操作

通过检查项目代码,发现大部分都是通过redisTemplate来操作reids,因为Spring的Template会在执行操作之后执行close代码,因此不存在此类情况

# 单独使用jedis的代码也都执行了close

仍有一小部分代码未使用redisTemplate,自己直接操作jedis,这类代码应该要在项目中避免,重复冗余的代码首先不利于维护,其次可能会在不经意间加入bug。

# 改善了一部分不规范代码

检查期间对于一些不规范代码,做了记录,排查完问题后,都将其进行处理。

# 推翻猜想

由于未检查到单独使用jedis并且未close的代码,此类情况也排除。

# 测试重现

单纯通过猜想去排查,容易大海捞针,其次由于问题发生在线上,不太好观察具体情况,因此写了测试代码对其进行重现

# 并发情况下容易重现

在提高并发量的情况下,问题更容易重现。

# apache commons pool2是否存在使用缺陷?

jedis使用apache commons pool2管理其连接池,那jedis对于commons pool是否存在使用缺陷呢?

# 检查对commons pool代码的使用

并未发现突出问题。

在此过程中发现commons pool之前是存在无法获取到pool中存放的对象的问题,但随后版本修复此问题,jedis中使用的版本也是最新release。

# 简单测试了commons pool2

测试其功能在并发下并无问题。

# jedis本身会否存在缺陷?

由于前面浏览过commons pool2的历史缺陷,因此也找到jedis的开源地(GitHub),发现最新版本已经更新为3.x,与应用使用的版本2.x差了一个大版本,在上面仔细对比与当前应用版本2.9.1,发现在其后仍然发布了多个版本,并且都是注明fix bug!看来这里终于发现了希望。

# 检查jedis前进的多个版本修复内容

通过对其多个修复内容的了解,发现此问题确实是由于并发下jedis的一个bug,并且已经修复

跳转到对应issue (opens new window)

# 修复上线

既然找到问题所在,便调整应用jedis版本,通过一轮测试,修复上线~

# attempt to unlock lock, not locked by current thread by node id的意思是?

英语还不差的同学, 根据提示语应该也看懂了, 这表示尝试去解锁, 但发现不是被当前线程锁住

# 场景重现

CountDownLatch countDownLatch = new CountDownLatch(2);
new Thread(()->{
    try {
        RLock lock = redissonClient.getFairLock(LOCK_KEY);
        log.warn("lock 1 want lock");
        //把持锁10s后就会释放
        lock.lock(10, TimeUnit.SECONDS);
        log.warn("lock 1...");
        try {
            TimeUnit.SECONDS.sleep(15);
        } catch (InterruptedException e) {
            log.error("", e);
        } finally {
            lock.unlock();
        }
    }catch (Exception e){
        log.error("", e);
    }
    countDownLatch.countDown();
}).start();

//保证一般情况下先执行上面的线程
try {
    TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
    e.printStackTrace();
}

new Thread(()->{
    try {
        RLock lock = redissonClient.getFairLock(LOCK_KEY);
        log.warn("lock 2 want lock");
        //把持锁10s后就会释放
        lock.lock(10, TimeUnit.SECONDS);
        log.warn("lock 2...");
        try {
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }catch (Exception e){
        log.error("", e);
    }
    countDownLatch.countDown();
}).start();
countDownLatch.await();

# 时间线

thread 1 thread 2
T1 拿到lock(leaseTime=10s), 开始sleep(15s)
T2 阻塞等待lock释放
T3 10s过后, 释放lock
T4 拿到lock(leaseTime=10s), 开始sleep(10s)
T5 sleep结束, 执行unlock, 抛出异常
T6 sleep结束, 执行unlock, 正常结束

# 分析

出现异常的问题在于T3时刻, 逻辑执行时间超过了加锁时间, 导致锁被超时释放, 此时同时有其他线程执行同一块代码或者如上面场景, 都会出现此异常.

# 解决方案

解决这个问题, 只需要在解锁时判断当前锁是否由当前线程持有就ok了. 但锁超时释放本身就是一个问题, 不能单纯使用这个方法来解决, 首先你得考虑:

  1. 逻辑是否允许另一条线程进入锁定范围? 不允许就需要考虑加长锁超时时间, 并缩短锁定逻辑处理时间
  2. 锁超时时间是否需要再从长计议
//判断当前锁是否由当前线程持有
lock.isHeldByCurrentThread();

//有些时候如果出现当前锁不被当前线程持有需要抛异常来做回滚等操作
if(!lock.isHeldByCurrentThread()){
    //throw new 
}
修改于: 8/11/2022, 3:17:56 PM