redis使用场景与可重入分布式锁介绍(干货)

时间:2024-05-20 07:27:44

redis使用场景与可重入分布式锁介绍(干货)

1、为什么使用

解决应用服务器的cpu和内存压力
减少io的读操作,减轻io的压力
关系型数据库的扩展性不强,难以改变表结构
2、适用场景:

数据高并发的读写
海量数据的读写
对扩展性要求高的数据
不适场景:

需要事务支持(非关系型数据库)
基于sql结构化查询储存,关系复杂

redis使用场景与可重入分布式锁介绍(干货)redis使用场景与可重入分布式锁介绍(干货
3:使用redis理由:

完全基于内存所以速度很快,网络层使用epoll解决高并发问题,单线程模型避免了不必要的上下文切换及竞争条件; 注意:单线程仅仅是说在网络请求这一模块上用一个请求处理客户端的请求,在持久化时它就会重开一个线程/进程去进行处理

丰富的数据类型,Redis有8种数据类型,常用的有 String、Hash、List、Set、 SortSet 这5种,都是基于键值的方式组织数据。每一种数据类型提供了非常丰富的操作命令,可以满足绝大部分需求

redis使用场景与可重入分布式锁介绍(干货)

redis使用场景与可重入分布式锁介绍(干货)String

字符串是最常用的数据类型,他能够存储任何类型的字符串,当然也包括二进制、JSON化的对象、甚至是Base64编码之后的图片。在Redis中一个字符串最大的容量为512MB.

常规key-value缓存应用; 常规计数:微博数,粉丝数等。

Hash

hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以使用hash 数据结构来存储用户信息,商品信息等等。

List

list 就是链表,Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。

Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。

换句话说,Redis的List实际是设计来用于实现队列,而不是用于实现类似ArrayList这样的列表的。如果你不是想要实现一个双端出入的队列,那么请尽量不要使用Redis的List数据结构。

Set

set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。

当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。

比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,

Sorted Set

和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。

举例: 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。

================================================================================

可重入锁:

首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:

redis使用场景与可重入分布式锁介绍(干货)
1、互斥性。在任意时刻,只有一个客户端能持有锁。

2、不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

3、具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。

4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

如果你通过网络搜索分布式锁,最多的就是基于redis的了。基于redis的分布式锁得益于redis的单线程执行机制,单线程在执行上就保证了指令的顺序化,所以很大程度上降低了开发人员的思考设计成本。但是,基于redis做分布式锁难道真的这么容易吗?

1.超时问题

在正常的业务当中,当一个线程获取到锁并且设置了锁的过期时间之后,会出现由于业务代码执行时间过长,锁由于到达超时时间自动释放的情况。自动释放之后,其他的线程就会获取到分布式锁,导致业务代码不会串行执行。如果业务上允许这样的情况偶尔发生,那程序员就开干吧,最后顶多人工干预一下,update 一下数据库。

为了避免这类情况发生,在使用redis分布式锁的时候,业务方应尽量避免长时间执行的代码任务。

如果设置锁的超时时间比较长,在一定程度上可以缓解业务代码执行时间长锁自动到期的问题,但是一旦业务代码down掉,其他等待锁的线程等待的时间会比较长,这种情况下,确保获取到锁的程序不会down 成为了主要问题。

2.获取锁失败

当锁被一个调用方获取之后,其他调用方在获取锁失败之后,是继续轮询还是直接业务失败呢?如果是继续轮询的话,同步情况下当前线程会一直处于阻塞状态,所以这里轮询的情况还是建议使用异步。

3.可重入性

可重入性是指已经拥有锁的客户端再次请求加锁,如果锁支持同一个客户端重复加锁,那么这个锁就是可重入的。如果基于redis的分布式锁要想支持可重入性,需要客户端封装,可以使用threadlocal存储持有锁的信息。这个封装过程会增加代码的复杂度,所以菜菜不推荐这样做。

4.redis挂了

如果在多个客户端获取锁的过程中,redis 挂了怎么办呢?假如一个客户端已经获取到了锁,这个时候redis挂了(假如是redis集群),其他的redis服务器会接着提供服务,这个时候其他客户端可以在新的服务器上获取到锁了,这也导致了锁意义的丢失。有兴趣的同学可以去看看RedLock,这种方案以牺牲性能的代价解决了这个问题。

RedLock算法加锁步骤如下

获取当前Unix时间,以毫秒为单位。
依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
5.时钟跳跃问题

在某些时候,redis的服务器时间发生的跳跃,由于锁的过期时间依赖于服务器时间,所以也会出现两个客户端同时获取到锁的情况发生。

6.原子操作

当把以上问题都有解决方案了之后,基于redis的分布式锁才可以放心使用。

===============================================================================

Redis锁的过期时间小于业务的执行时间该如何续期?

默认情况下,加锁的时间是30秒.如果加锁的业务没有执行完,那么到 30-10 = 20秒的时候,就会进行一次续期,把锁重置成30秒.那这个时候可能又有同学问了,那业务的机器万一宕机了呢?宕机了定时任务跑不了,就续不了期,那自然30秒之后锁就解开了呗.

redis使用场景与可重入分布式锁介绍(干货)

redis使用场景与可重入分布式锁介绍(干货)1)加锁机制

咱们来看上面那张图,现在某个客户端要加锁。如果该客户端面对的是一个redis cluster集群,他首先会根据hash节点选择一台机器。

这里注意,仅仅只是选择一台机器!这点很关键!

为啥要用lua脚本呢?

因为一大坨复杂的业务逻辑,可以通过封装在lua脚本中发送给redis,保证这段复杂业务逻辑执行的原子性。

(2)锁互斥机制

那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?

此时客户端2会进入一个while循环,不停的尝试加锁。

(3)watch dog自动延期机制

客户端1加锁的锁key默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?

简单!只要客户端1一旦加锁成功,就会启动一个watch dog看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key,那么就会不断的延长锁key的生存时间。

(4)释放锁机制

如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。

其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。

redis使用场景与可重入分布式锁介绍(干货)

redis使用场景与可重入分布式锁介绍(干货)
以上就是redis使用场景和redis分布式锁介