无常是常

分享技术见解、学习心得和生活感悟

最新文章

Redis分布式锁

简介

在一些分布式系统中,应用与应用之间是相互独立部署的,Java应用运行在不同的JVM中,所以,在操作一些共享资源的时候,使用JDK提供的Lock工具类时就有些力不从心,这时候就需要借助外力来实现分布式一致性问题。

通常会使用以下三种方式进行实现:

  • 基于数据库实现分布式锁(悲观锁机制)
  • Zookeeper分布式锁
  • Redis分布式锁

下面简单对比几种方式的优缺点:

方式 优点 缺点
数据库 实现简单、易于理解 对数据库压力大
Redis 易于理解 自己实现、不支持阻塞
Zookeeper 支持阻塞 需要理解Zookeeper、程序复杂
Curator 提供锁的方法 依赖Zookeeper、强一致
Redisson 提供锁的方法、可阻塞

安全和活性的保证

Redis官方文档提出以下三点作为分布式锁的最低保证:

  1. 互斥,在任何给定时刻,只有一个客户端可以持有锁
  2. 无死锁,最终,即使锁定资源的客户端崩溃或分区,也始终可以获得锁
  3. 容错能力,只要大多数Redis节点都处于运行状态,客户端就可以获取和释放锁

基于主备架构实现的不足

使用Redis实现分布式锁最简单的方法就是加锁的时候创建一个带有过期时间Key(这样是为了防止出现死锁),当客户端需要释放锁的时候删除这个Key。

从表面上看没有什么问题,但是当Redis出现宕机的时候怎么办?为了解决单点故障问题,我们可以添加一个从节点,当主节点不可用的时候,切换到从节点,但是这样实际上是不可行的,因为Redis使用的是异步复制。

该模型明显存在的竞争条件:

  1. 客户端A获取主节点的锁
  2. 主节点将Key同步到从节点之前发生宕机
  3. 从节点切换成主节点
  4. 客户端B获取到相同资源的锁,此时A和B同时持有锁,违反了安全性

单实例方案

假设可以克服以上单节点不足的问题,我们可以使用以下命令实现分布式锁:

1
SET resource_name my_random_value NX PX 30000

该命令仅在Key不存在(NX)、且到期时间(PX)为30000毫秒的情况下才设置Key。Key的值为一个随机数,该值要求必须全局唯一,使用全局唯一值是为了在删除Key的时候,Key的值是我们之前设置的值时,才删除Key。(Tips:我们总不能删除其他客户端设置的Key吧?)

可以使用以下Lua脚本完成,因为Lua脚本可以保证两个操作的原子性。

1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

RedLock算法

在算法的分布式版本中,我们假设有5个Master节点,这些节点是完全独立的,我们将各个节点部署在不同的服务器中,以保证他们同时出现故障的概率。

为了获取锁,客户端执行以下操作:

  1. 客户端获取当前时间的时间戳
  2. 客户端尝试在N(N=5)个节点上以相同的Key和Value获取一个锁(此处和单实例方式相同)
  3. 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间,当客户端能够在大多数实例(至少3个)中获取锁时 ,并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁。
  4. 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间
  5. 如果客户端由于某种原因(无法锁定N / 2 + 1实例或有效时间为负数)而未能获得该锁,它将尝试解锁所有实例

释放锁

释放锁很简单,只需在所有实例中释放锁(即使之前在某个实例中没有获取到锁)。

RedLock注意点

  1. 先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移
  2. 对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况,从而使锁失效
  3. 一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效

系统具有活性的三大特征

  1. 能够自动释放锁
  2. 再获取锁失败(不到一半以上),或任务完成后能够释放锁,不用等到其自动过期
  3. 再客户端重试获取锁之前(第一次失败到第二次失败之间的间隔时间)大于获取锁消耗的时间

参考Redis官方文档 https://redis.io/topics/distlock
RedLock分析 [http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html][http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html]

简介

在一些分布式系统中,应用与应用之间是相互独立部署的,Java应用运行在不同的JVM中,所以,在操作一些共享资源的时候,使用JDK提供的Lock工具类时就有些力不从心,这时候就需要借助外力来实现分布式一致性问题。

通常会使用以下三种方式进行实现:

  • 基于数据库实现分布式锁(悲观锁机制)
  • Zookeeper分布式锁
  • Redis分布式锁

下面简单对比几种方式的优缺点:

方式 优点 缺点
数据库 实现简单、易于理解 对数据库压力大
Redis 易于理解 自己实现、不支持阻塞
Zookeeper 支持阻塞 需要理解Zookeeper、程序复杂
Curator 提供锁的方法 依赖Zookeeper、强一致
Redisson 提供锁的方法、可阻塞

安全和活性的保证

Redis官方文档提出以下三点作为分布式锁的最低保证:

  1. 互斥,在任何给定时刻,只有一个客户端可以持有锁
  2. 无死锁,最终,即使锁定资源的客户端崩溃或分区,也始终可以获得锁
  3. 容错能力,只要大多数Redis节点都处于运行状态,客户端就可以获取和释放锁

基于主备架构实现的不足

使用Redis实现分布式锁最简单的方法就是加锁的时候创建一个带有过期时间Key(这样是为了防止出现死锁),当客户端需要释放锁的时候删除这个Key。

从表面上看没有什么问题,但是当Redis出现宕机的时候怎么办?为了解决单点故障问题,我们可以添加一个从节点,当主节点不可用的时候,切换到从节点,但是这样实际上是不可行的,因为Redis使用的是异步复制。

该模型明显存在的竞争条件:

  1. 客户端A获取主节点的锁
  2. 主节点将Key同步到从节点之前发生宕机
  3. 从节点切换成主节点
  4. 客户端B获取到相同资源的锁,此时A和B同时持有锁,违反了安全性

单实例方案

假设可以克服以上单节点不足的问题,我们可以使用以下命令实现分布式锁:

1
SET resource_name my_random_value NX PX 30000

该命令仅在Key不存在(NX)、且到期时间(PX)为30000毫秒的情况下才设置Key。Key的值为一个随机数,该值要求必须全局唯一,使用全局唯一值是为了在删除Key的时候,Key的值是我们之前设置的值时,才删除Key。(Tips:我们总不能删除其他客户端设置的Key吧?)

可以使用以下Lua脚本完成,因为Lua脚本可以保证两个操作的原子性。

1
2
3
4
5
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

RedLock算法

在算法的分布式版本中,我们假设有5个Master节点,这些节点是完全独立的,我们将各个节点部署在不同的服务器中,以保证他们同时出现故障的概率。

为了获取锁,客户端执行以下操作:

  1. 客户端获取当前时间的时间戳
  2. 客户端尝试在N(N=5)个节点上以相同的Key和Value获取一个锁(此处和单实例方式相同)
  3. 客户端通过从当前时间中减去在步骤1中获得的时间戳,来计算获取锁所花费的时间,当客户端能够在大多数实例(至少3个)中获取锁时 ,并且获取锁所花费的总时间小于锁有效时间,则认为已获取锁。
  4. 如果获取了锁,则将其有效时间视为初始有效时间减去经过的时间
  5. 如果客户端由于某种原因(无法锁定N / 2 + 1实例或有效时间为负数)而未能获得该锁,它将尝试解锁所有实例

释放锁

释放锁很简单,只需在所有实例中释放锁(即使之前在某个实例中没有获取到锁)。

RedLock注意点

  1. 先假设client获取所有实例,所有实例包含相同的key和过期时间(TTL) ,但每个实例set命令时间不同导致不能同时过期,第一个set命令之前是T1,最后一个set命令后为T2,则此client有效获取锁的最小时间为TTL-(T2-T1)-时钟漂移
  2. 对于以N/2+ 1(也就是一半以 上)的方式判断获取锁成功,是因为如果小于一半判断为成功的话,有可能出现多个client都成功获取锁的情况,从而使锁失效
  3. 一个client锁定大多数事例耗费的时间大于或接近锁的过期时间,就认为锁无效,并且解锁这个redis实例(不执行业务) ;只要在TTL时间内成功获取一半以上的锁便是有效锁;否则无效

系统具有活性的三大特征

  1. 能够自动释放锁
  2. 再获取锁失败(不到一半以上),或任务完成后能够释放锁,不用等到其自动过期
  3. 再客户端重试获取锁之前(第一次失败到第二次失败之间的间隔时间)大于获取锁消耗的时间

参考Redis官方文档 https://redis.io/topics/distlock
RedLock分析 [http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html][http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html]

Redis哨兵

Sentinel的分布式特征

Redis Sentinel是一个分布式系统:
Sentinel本身的设计是在为多个Sentinel进程协同合作的配置中运行。

  • 当多个哨兵就给定的主机不再可用的事实达成共识时,将执行故障检测,降低了误报的可能性。
  • 即使不是所有的Sentinel进程都在工作,Sentinel仍可正常工作,从而使系统能够应对故障。毕竟,拥有故障转移系统本身就是一个单点故障,

运行哨兵

1
redis-sentinel /path/to/sentinel.conf

redis-sentinelredis-server的一个软链接,所以也可以使用以下方法:

1
redis-server /path/to/sentinel.conf --sentinel

两种方法的工作原理相同。

但是,再运行Sentinel必须指定配置文件,因为系统将使用此文件来保存当前状态,以便在重新启动时重新加载。如果未指定文件,则会启动失败。

Sentinels默认情况下会监听TCP端口26379连接,因此必须打开26379端口。

Sentinel的基础知识

  1. 一个健壮的集群至少需要三个Sentinel实例
  2. 应该将三个Sentinel实例部署在不同的机器上
  3. 因为Redis使用的是异步复制,所以不能保证在故障转移期间保证数据的写入。

配置哨兵

Redis的源码包包含一个sentinel.conf文件可用于配置Sentinel,典型的最小配置如下所示:

1
2
3
4
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor含义如下:

1
sentinel monitor <master-group-name> <ip> <port> <quorum>

<master-group-name> 指主节点名称,<ip> IP地址 <port>端口号,重点说一下<quorum>
假如有5个Sentinel进程,并且给定主服务器的quorum置为2,则将发生以下情况:

  • 如果有两个哨兵同时发现主节点不可访问,则其中一个哨兵将尝试启动故障转移。
  • 如果有三个哨兵同时发现主节点不可访问,则将启动故障转移。
1
sentinel down-after-milliseconds

down-after-milliseconds是指Sentinel在指定的时间内没有获得实例的响应,则认为实例已关闭。

1
sentinel parallel-syncs

parallel-syncs 设置每次可以对几个副本进行同步数据

Redis 哨兵部署示例

经典三节点最小部署

1
2
3
4
5
6
7
8
9
10
11
        +----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+

Configuration: quorum = 2

如果主M1发生故障,则S2和S3将达成协议,并能够开启故障转移,从而使客户端能够继续使用。

模拟发生网络分区

1
2
3
4
5
6
7
8
9
10
11
12
         +----+
| M1 |
| S1 | <- C1 (writes will be lost)
+----+
|
/
/
+------+ | +----+
| [M2] |----+----| R3 |
| S2 | | S3 |
+------+ +----+

在这种情况下,网络分区隔离了旧的主数据库M1,因此副本R2被提升为主数据库。但是,与旧主服务器位于同一分区中的客户端(例如C1)可能会继续向旧主服务器写入数据。该数据将永远丢失,因为当分区恢复正常时,主服务器将被重新配置为新主服务器的副本,从而造成数据丢失。

使用以下Redis复制功能可以缓解此问题,如果主服务器检测到副本数量没有达到指定的副本数据,则停止接受数据写入。

1
2
3
4
# 最小副本数量
min-replicas-to-write 1
# 发送异步确认的最大时间
min-replicas-max-lag 10

Sentinel的分布式特征

Redis Sentinel是一个分布式系统:
Sentinel本身的设计是在为多个Sentinel进程协同合作的配置中运行。

  • 当多个哨兵就给定的主机不再可用的事实达成共识时,将执行故障检测,降低了误报的可能性。
  • 即使不是所有的Sentinel进程都在工作,Sentinel仍可正常工作,从而使系统能够应对故障。毕竟,拥有故障转移系统本身就是一个单点故障,

运行哨兵

1
redis-sentinel /path/to/sentinel.conf

redis-sentinelredis-server的一个软链接,所以也可以使用以下方法:

1
redis-server /path/to/sentinel.conf --sentinel

两种方法的工作原理相同。

但是,再运行Sentinel必须指定配置文件,因为系统将使用此文件来保存当前状态,以便在重新启动时重新加载。如果未指定文件,则会启动失败。

Sentinels默认情况下会监听TCP端口26379连接,因此必须打开26379端口。

Sentinel的基础知识

  1. 一个健壮的集群至少需要三个Sentinel实例
  2. 应该将三个Sentinel实例部署在不同的机器上
  3. 因为Redis使用的是异步复制,所以不能保证在故障转移期间保证数据的写入。

配置哨兵

Redis的源码包包含一个sentinel.conf文件可用于配置Sentinel,典型的最小配置如下所示:

1
2
3
4
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

sentinel monitor含义如下:

1
sentinel monitor <master-group-name> <ip> <port> <quorum>

<master-group-name> 指主节点名称,<ip> IP地址 <port>端口号,重点说一下<quorum>
假如有5个Sentinel进程,并且给定主服务器的quorum置为2,则将发生以下情况:

  • 如果有两个哨兵同时发现主节点不可访问,则其中一个哨兵将尝试启动故障转移。
  • 如果有三个哨兵同时发现主节点不可访问,则将启动故障转移。
1
sentinel down-after-milliseconds

down-after-milliseconds是指Sentinel在指定的时间内没有获得实例的响应,则认为实例已关闭。

1
sentinel parallel-syncs

parallel-syncs 设置每次可以对几个副本进行同步数据

Redis 哨兵部署示例

经典三节点最小部署

1
2
3
4
5
6
7
8
9
10
11
        +----+
| M1 |
| S1 |
+----+
|
+----+ | +----+
| R2 |----+----| R3 |
| S2 | | S3 |
+----+ +----+

Configuration: quorum = 2

如果主M1发生故障,则S2和S3将达成协议,并能够开启故障转移,从而使客户端能够继续使用。

模拟发生网络分区

1
2
3
4
5
6
7
8
9
10
11
12
         +----+
| M1 |
| S1 | <- C1 (writes will be lost)
+----+
|
/
/
+------+ | +----+
| [M2] |----+----| R3 |
| S2 | | S3 |
+------+ +----+

在这种情况下,网络分区隔离了旧的主数据库M1,因此副本R2被提升为主数据库。但是,与旧主服务器位于同一分区中的客户端(例如C1)可能会继续向旧主服务器写入数据。该数据将永远丢失,因为当分区恢复正常时,主服务器将被重新配置为新主服务器的副本,从而造成数据丢失。

使用以下Redis复制功能可以缓解此问题,如果主服务器检测到副本数量没有达到指定的副本数据,则停止接受数据写入。

1
2
3
4
# 最小副本数量
min-replicas-to-write 1
# 发送异步确认的最大时间
min-replicas-max-lag 10