• 微信公众号:美女很有趣。 工作之余,放松一下,关注即送10G+美女照片!

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

互联网 diligentman 2周前 (02-21) 11次浏览

文章目录

  • 上一章
  • 一、Redis事务
    • 示例1 正常执行:
    • 示例2 放弃事务:
    • 示例3 事务队列中存在命令性错误则所有命令都不会执行
    • 示例4 事务队列中存在语法性错误则其他正确命令会被执行,错误命令抛出异常。
    • 示例5 使用watch
    • 示例6 使用watch被打断
  • 二、分布式锁
    • 2.1 INCR方法
    • 2.2 SETNX方法
      • 2.2.1 加锁SETNX
      • 2.2.2 获取锁SETNX
      • 2.2.3 释放锁
    • 2.3 SET 方法
    • 2.4 注意分布式锁中的问题
    • 2.5 定时任务重复执行
    • 2.6 避免用户重复下单
  • 三、分布式自增 ID
  • 四、Redis 实现消息队列
  • 五、Redis 实现延时队列
  • 下一章

上一章

深入学习Redis_(一)五种基本数据类型、RedisTemplate、RedisCache、缓存雪崩等

深入学习Redis_(二)淘汰策略、持久化机制、主从复制、哨兵模式等

一、Redis事务

Redis 通过 MULTIEXECWATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后 一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。原子性 (Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
Redis事务没有隔离级别的概念。
Redis不保证原子性:
Redis中,单条命令是原子性执行的,但事务不保证原子性,且没有回滚。事务中任意命令执行失败,其余的命令仍会被执行。

  1. watch key1 key2 ... : 监视一或多个key,如果在事务执行之前,被监视的key被其他命令改动,则事务被打断 ( 类似乐观锁 )
  2. multi : 标记一个事务块的开始( queued )
  3. exec : 执行所有事务块的命令 ( 一旦执行exec后,之前加的监控锁都会被取消掉 )
  4. discard : 取消事务,放弃事务块中的所有命令
  5. unwatch : 取消watch对所有key的监控

示例1 正常执行:

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

示例2 放弃事务:

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

示例3 事务队列中存在命令性错误则所有命令都不会执行

如果在事务队列中存在命令性错误则执行EXEC命令时,所有命令都不会执行
深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

示例4 事务队列中存在语法性错误则其他正确命令会被执行,错误命令抛出异常。

如果事务队列中存在语法性错误,则执行EXEC命令时,其他正确命令会被执行,错误命令抛出异常
深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

示例5 使用watch

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

示例6 使用watch被打断

使用watch检测inMoney,在开启事务后,在新窗口执行更改inMoney值的操作,,模拟其他客户端在事务执行期间更改watch监控的数据,然后再执行EXEC后,事务未成功执行。
步骤一:
深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等
步骤二:
深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

步骤三:

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

二、分布式锁

2.1 INCR方法

这种是比较简单的加锁方式:
key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。 然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 1 ,说明这个锁正在被使用当中。
INCR key方法:
将键存储的数字增加1。如果该键不存在,则在执行操作前将其设置为0。
返回值
整数回复:自增后的key值

EXPIRE key seconds方法:
设置键的超时时间。超时后,将被自动删除。

	1、 客户端A请求服务器获取key的值为1表示获取了锁
    2、 客户端B也去请求服务器获取key的值为2表示获取锁失败
    3、 客户端A执行代码完成,删除锁
    4、 客户端B在等待一段时间后在去请求的时候获取key的值为1表示获取锁成功
    5、 客户端B执行代码完成,删除锁
 
    $redis->incr($key);
    $redis->expire($key, $ttl); //设置生成时间为1秒

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

2.2 SETNX方法

SETNX是“SET if Not eXists”的缩写。
SETNX key val 方法:

Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed. SETNX is short for “SET if Not eXists”.
Return value
Integer reply, specifically:
1 if the key was set
0 if the key was not set

如果key不存在,则将key设置为指定值。在这种情况下,它等于SET。当key已经保存一个值时,不执行任何操作。
如果返回1说明set成功,即原来是没有这个key的;返回0说明set失败,key是存在的。
示例:

redis> SETNX mykey "Hello"
(integer) 1
redis> SETNX mykey "World"
(integer) 0
redis> GET mykey
"Hello"
redis> 

深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

2.2.1 加锁SETNX

获取锁的时候,使用 setnx加锁,锁的 value 值为一个随机生成的 UUID,在释放锁的时候进行判断。并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁。

2.2.2 获取锁SETNX

获取锁的时候调用 setnx,如果返回 0,则该锁正在被别人使用,返回 1 则成功获取锁。 还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

2.2.3 释放锁

释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 DEL 进行锁释放。

2.3 SET 方法

上面两种方法都有一个问题,会发现,都需要设置 key 过期。那么为什么要设置key过期呢?如果请求执行因为某些原因意外退出了,导致创建了锁但是没有删除锁,那么这个锁将一直存在,以至于以后缓存再也得不到更新。于是乎我们需要给锁加一个过期时间以防以外。
但是借助 Expire 来设置就不是原子性操作了。所以还可以通过事务来确保原子性,但是还是有些问题,所以官方就引用了另外一个,使用 SET 命令本身已经从版本 2.6.12 开始包含了设置过期时间的功能。
SET key value [EX seconds|PX milliseconds|KEEPTTL] [NX|XX] [GET] 方法:
设置key以保存字符串值。如果key已经保存了一个值,那么不管它的类型是什么,它都会被覆盖。如果SET操作成功,与该键关联的任何以前的生存时间都将被丢弃。
其他参数
EX seconds——设置指定的过期时间,以为单位。
PX milliseconds——设置指定的过期时间,以毫秒为单位。
NX——只在键不存在时设置它。
XX——只设置已经存在的键。
KEEPTTL——保留与该键相关联的生存时间。
GET——返回存储在key处的旧值,如果key不存在则返回nil。

>= 2.6.12: Added the EX, PX, NX and XX options.
>= 6.0: Added the KEEPTTL option.
>= 6.2: Added the GET option.

使用命令SET resource-name anystring NX EX max-lock-time是Redis实现锁定系统的一个简单方法。

如果上面的命令返回OK(或者如果命令返回Nil,则在一段时间后重试),客户端就可以获得锁,并使用DEL删除锁。
到达过期时间后,锁将自动释放。
使用只在值匹配时删除键的Lua脚本:

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

2.4 注意分布式锁中的问题

场景:
设置锁过期时间为 N 秒,A 线程拿到锁,业务执行时间超过 N 秒时,锁过期被释放,B 线程拿到锁,A 线程执行完毕释放掉锁,C 线程又拿到锁

这里有两个问题,一是加锁与释放锁混乱,二是未执行完毕逻辑锁被过期释放

解决方法:
第一个问题:
解铃还须系铃人,解决加锁与释放混乱,可以采用将 value 设置为随机数(UUID)的方式,A 线程设置的锁,只能由 A 线程释放,释放锁时先判断 value 是否一致。
第二个问题:
我们加锁的过程是 加锁—执行业务代码—释放锁,如果加入业务代码的执行时间超时了,是不是业务代码还没有执行完,锁就已经释放了。放在购票场景中,第一位旅客还没有完成购票,第二位旅客就开始购票。显然不合理。
可以在业务代码中开辟一个“续命”的操作:
这里我们需要估计业务代码的执行时间,加入预估出来的时间是10秒
加锁 set key value ex 10 nx
每过3秒,把该锁的时间重新设置为 10秒
执行业务代码
释放锁 del lock
这里的续命时间间隔 = 过期时间 10S / 3

分布式锁可以避免不同进程重复相同的工作,减少资源浪费。 同时分布式锁可以避免破坏数据正确性的发生, 例如多个进程对同一个订单操作,可能导致订单状态错误覆盖。应用场景如下。

2.5 定时任务重复执行

如果我们需要一个定时任务来进行订单状
态的统计。比如每 15 分钟统计一下所有未支付的订单数量。那么我们启动定时任务的时候,肯定不能同一时刻多个业务后台服务都去执行定时任务, 这样就会带来重复计算以及业务逻辑混乱的问题。
这时候,就需要使用分布式锁,进行资源的锁定。那么在执行定时任务的函数中,首先进行分布式锁的获取,如果可以获取的到,那么这台机器就执行正常的业务数据统计逻辑计算。如果获取不到则证明目前已有其他的服务进程执行这个定时任务,就不用自己操作执行了,只需要返回就行了。
深入学习Redis_(三)事务、分布式锁、消息队列、延时队列等

2.6 避免用户重复下单

分布式实现方式有很多种:

  1. 数据库乐观锁方式
  2. 基于 Redis 的分布式锁
  3. 基于 ZK 的分布式锁

分布式锁实现要保证几个基本点。
4. 互斥性:任意时刻,只有一个资源能够获取到锁。
5. 容灾性:能够在未成功释放锁的的情况下,一定时限内能够恢复锁的正常功能。
6. 统一性:加锁和解锁保证同一资源来进行操作。

三、分布式自增 ID

通常对于分布式自增 ID 的实现方式有下面几种:

  1. 利用数据库自增 ID 的属性
  2. 通过 UUID 来实现唯一 ID 生成
  3. Twitter 的 SnowFlake 算法(雪花算法
  4. 利用 Redis 生成唯一 ID

使用 Redis 的 INCR 命令来实现唯一ID。Redis 是单进程单线程架构,不会因为多个取号方的 INCR 命令导致取号重复。因此,基于 Redis
的 INCR 命令实现序列号的生成基本能满足全局唯一与单调递增的特性。

四、Redis 实现消息队列

使用 list 类型保存数据信息,rpush 生产消息,lpop 消费消息,当 lpop 没有消息时,可 以 sleep 一段时间,然后再检查有没有信息,如果不想 sleep 的话,可以使用 blpop, 在没 有信息的时候,会一直阻塞,直到信息的到来。redis 可以通过 pub/sub 主题订阅模式实现 一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。

RPUSH key element [element …] 方法:
在存储在key的列表尾部插入所有指定的值。如果key不存在,则在执行推操作之前将其创建为空列表。当key保存的值不是列表时,返回一个错误。
使用时,在命令的末尾指定多个参数,就可以推入多个元素。元素一个接一个地插入到列表的尾部,从最左边的元素到最右边的元素。例如,命令RPUSH mylist a b c将生成一个包含a作为第一个元素、b作为第二个元素、c作为第三个元素的列表。
LPOP key [count] 方法:
删除并返回存储在key中的列表的第一个元素。
默认情况下,该命令从列表的开头弹出单个元素。当提供了可选的count参数时,将返回多个的元素,这取决于列表的长度。

五、Redis 实现延时队列

使用 SortedSet(ZSet),使用时间戳score, 消息内容作为 key,调用 zadd 来生产消息,消费者 使用 zrangbyscore 获取 n 秒之前的数据做轮询处理。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]方法的解释:

Returns all the elements in the sorted set at key with a score between min and max (including elements with score equal to min or max). The elements are considered to be ordered from low to high scores.
The elements having the same score are returned in lexicographical order (this follows from a property of the sorted set implementation in Redis and does not involve further computation).
As per Redis 6.2.0, this command is considered deprecated. Please prefer using the ZRANGE command with the BYSCORE argument in new code.
The optional LIMIT argument can be used to only get a range of the matching elements (similar to SELECT LIMIT offset, count in SQL). A negative count returns all elements from the offset. Keep in mind that if offset is large, the sorted set needs to be traversed for offset elements before getting to the elements to return, which can add up to O(N) time complexity.
The optional WITHSCORES argument makes the command return both the element and its score, instead of the element alone. This option is available since Redis 2.0.

返回key中分数在min和max之间的排序集合中的所有元素(包括分数等于min或max的元素)。元素被认为是从低到高的分数排序
具有相同分数的元素按字典顺序返回(这遵循Redis中排序集实现的一个属性,不涉及进一步的计算)。
根据Redis 6.2.0,这个命令被认为已弃用。请在新代码中使用带BYSCORE参数的ZRANGE命令。
可选的LIMIT参数只能用于获取一个范围内的匹配元素(类似于SQL中的SELECT LIMIT offset, count)。负数的计数将从偏移量中返回所有元素。请记住,如果偏移量很大,在获得要返回的元素之前,需要遍历排序集的偏移量元素,这可能会增加O(N)的时间复杂度。
可选的WITHSCORES参数使该命令同时返回元素及其分数,而不仅仅是元素本身。这个选项从Redis 2.0开始就可用了。

简单来说就是:使用ZSet类型,在存入的时候分数使用当时的时间戳,查询的时候以现在的时间戳减去延时的时间(比如五分钟)的时间戳为分数轮询即可。

下一章

深入学习Redis_(四)Redis与Lua脚本


喜欢 (0)