Redis从入门到精通(2):事务、过期时间、sort以及队列

Redis事务

Redis中的事务是一组命令的集合,事务最基本的特性就是原子性。一个事务中的命令要么全都执行,要么全部不执行。银行转账是理解事务最常见的一个例子,我已经在以前的文章中讲过很多次事务,这里就不再细讲,我们直接来看Redis的事务怎样来使用。

Redis事务的原理是先将属于一个事务的命令发送给Redis,然后再让Redis依次执行这些命令。

例如:
		MULTI
			SADD test  1
			SADD test  2
		EXEC
		
MULTI命令告诉Redis,接下来我发送的命令属于同一个事务,先不要执行,而是暂时存起来。
然后我们发送命令,Redis返回QUEUED表示这两条命令已经进入等待执行的事务队列。
当在同一个事务中执行的命令都发送给Redis后,使用EXEC命令告诉Redis将事务队列中的所有命令按照顺序执行。
EXEC命令的返回值就是这些命令的返回值组成的列表。

在这里插入图片描述
Redis事务的错误处理

如果事务中的某个命令出错,那么Redis会怎样进行处理呢?
(1)语法错误

	例子:
	
		MULTI
			SET KEY VALUE
			SET KEYY 
		EXEC 

在这里插入图片描述
只要有语法错误,所有的命令都不会被执行,并且执行EXEC后会直接返回错误。

(2)运行错误
运行错误指在命令执行时出现的错误,比如使用散列类型的命令操作集合类型的键,这种错误在实际执行之前Redis是无法发现的。
如果事务里的一条命令出现了运行错误,事务里的其他命令依然会继续执行。

	例子:
	
		MULTI
			SET KEY 1
			SADD KEY 2
			SET KEY 3
		EXEC 

在这里插入图片描述
可见虽然SADD key 2出现了错误,但是 set key 3依然执行了。

Redis的事务并不支持出错后回滚功能,因此我们必须在事务执行出错后自己收拾烂摊子.

现在我们来假设一种情况,来解决一个问题。我们假设Redis中并未提供INCR命令来完成键值的原子性递增,如果要实现该功能,我们只能来自行编写相应的代码。伪代码如下:

 	伪代码:
			val = GET mykey
      		val = val + 1
      		SET mykey $val

我们以前说过Redis所有的命令都是原子操作,但是多个命令组合起来就不再是原子操作。
所以以上代码只有在单连接的情况下才可以保证执行结果是正确的,因为如果在同一时刻有多个客户端在同时执行该段代码,那么就会出现多线程程序中经常出现的一种错误场景–竞态争用(race condition)。

比如,客户端A和B都在同一时刻读取了mykey的原有值,假设该值为10,此后两个客户端又均将该值加一后set回Redis服务器,这样就会导致mykey的结果为11,而不是正确的12。

我们理所当然的会想到说用我们刚学的事务来解决这个问题。但我们别忘了,执行事务时只有所有命令都依次执行完后才能得到每个结果的返回值。而我们必须得先得到值,然后再去执行下一步的操作。
而Redis为我们提供了一个WATCH命令可以帮助我们解决这个问题。

WATCH key1 key2...: 该命令可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不会被执行,监控一直持续到EXEC命令。 

这样当我们获取到值的时候,如果中间被其他客户端改变了,那么事务就不再执行。

例子:
			SET key 1
			WATCH key
			SET key 2
			MULTI
			SET key 3
			EXEC 
			GET key  

在这里插入图片描述
事务执行前key的值改变了,所以事务不会执行。
那么我们上面的实现INCR功能的代码就应该改造如下

	  WATCH mykey
      val = GET mykey
      val = val + 1
      MULTI
      SET mykey $val
      EXEC

	 //别的客户端只要对mukey进行了更改,我这边就不执行任何操作。

执行EXEC命令会取消对所有键的监控,如果不想执行事务中的命令也可以使用UNWATCH来取消监控。

实际案例

日常生活中商品秒杀的例子。假设我们某个物品有五个秒杀名额。我们肯定是要先判断当前名额还剩多少,然后来决定下一步是继续秒杀活动还是关闭。

伪代码:

		WATCH count
		count =GET("killing")
		MULTI 
		if count>0 
			decr count 
			EXEC 
		else 
			结束
	(2)我们自己来实现一个HSETNX的功能。
	  WATCH key
	  isExists=HEXISTS key  field
	  if isExists==1
		  MULTI
		  HSET key field value
		  exec
	  else
		  UNWATCH
在代码中会判断要赋值的字段是否存在,如果不存在就不执行事务中的操作,但需要使用UNWATCH来保证下一个事务的执行不会受到影响。

过期时间

在实际的开发中经常会遇到一些有时效的数据,例如验证码、缓存等等。Redis中为我们提供了很简单的命令来设置键的过期时间。

	EXPIRE key second:second为键的过期时间,单位是秒,必须是整数,到期后会自动删除。
	返回值为1则表示设置成功,0则表示键不存在或设置失败。
	
	TTL key:查看键还有多久的时间会被删除,返回值是键的剩余时间,单位为秒
	当键不存在时返回-2,没有为键设置过期时间则返回-1.
	
	PERSIST key:取消键的过期时间设置(即把键恢复为永久的)。如果成功设置则返回1,否则返回0(键不存在或者本身就是永久的)
	使用SET命令为键赋值也会同时清除键的过期时间。
	
	PEXPIRE key time:和expire一样,区别是单位是毫秒,可以更精确的控制时间。
	可以用PTTL以毫秒为单位返回键的剩余时间。
	
	如果使用WATCH命令监控了一个拥有过期时间的键,该键时间到期自动删除并不会被WATCH命令认为该键改变。
	
	EXPIREAT key time:该命令使用unix时间作为参数表示键的过期时间。
	
	PEXPIREAT key time:与上面命令的区别是单位是毫秒。

在这里插入图片描述
内部原理:当一个键过期后,客户端尝试访问已过期键时,Redis会立即将其从内存中删除。Redis这种删除键的方式被称为被动过期。对于那些已经过期且永远不会再被访问到的键,Redis还会定期地运行一个基于概率的算法来进行主动删除。具体地说,Redis会随机选择设置了过期时间的20个键,在这20个被选中的键中,已过期的会立即删除,如果选中的键有超过25%的键已经过期且被删除,那么Redis会再次重复这个过程。默认情况下,上述过程每秒运行10次,可以通过配置文件的hz的值进行设置。

实际案例

(1)为了减轻服务器的压力,需要限制每个用户(以ip计算)一段时间的最大访问量。
例如我们限制每个用户每分钟只可以访问一百次,我们可以使用一个rate:用户ip的字符串类型键
每次用户访问就incr该键,如果递增后的键值为1,则设置过期时间为一分钟。
每次访问页面就读取该值,超过一百则提示用户。

	伪代码如下:
	
		isExists=EXISTS rate:ip
		if isExists=1
			times=INCR rate:ip
			if times>100
				print:访问频率过快
		else
			INCR rate:ip
			EXPIRE rate:ip 60

这段代码其实有一个小问题,就是用户执行完倒数第二行以后,因为某些意外退出没有继续执行,那么该键就会永久保存,导致用户永远最多只能访问一百次。我们可以用上我们学的事务。

伪代码如下:

		isExists=EXISTS rate:ip
		if isExists=1
			times=INCR rate:ip
			if times>100
				print:访问频率过快
		else
			MULTI
			INCR rate:ip
			EXPIRE rate:ip 60
			EXEC

事实上,上面的代码仍然存在一些比较极端的问题。
假设用户每分钟只能访问十次。用户在第一分钟的第一秒访问了一次,在最后一秒访问了九次。
在第二分钟的第一秒访问了十次。
这种情况我们现在的访问频率限制完全无效,但实际上该用户在两秒内访问了19次
所以如果要精确的控制用户每分钟只能访问10次,我们需要记录下用户每次访问的时间。
对每个用户,我们使用一个列表类型的键来记录最近十次访问的时间。
一旦键中的元素超过十个,就判断时间最早的元素距现在的时间是否小于一分钟,如果是则表示最近一分钟内访问次数超过了十次。
如果不是就将现在的时间加入到列表之中,同时把最早的时间给删除

伪代码如下:
		listLength=LLEN rate:ip
		if listLength<10
			LPUSH rate:ip now()
		else
			time=LINDEX rate:ip  -1
			if now()-time<60
				print:访问频率过快
			else
				LPUSH rate:ip now()
				LTRIM  rate:ip 0  9
		

当要限制A时间内最多访问B次,如果B的数值过大,会占用比较多的存储空间,并且这个上面的方法还会存在竞态问题,我们在后面用脚本可以很好的处理。

(2)实现缓存

为了提高网站的负载能力,常常需要将一些访问频率高对资源消耗比较大的结果给缓存起来,并希望让缓存过段时间自动过期。

例如我们计算成绩排名,每过一个小时更新一次
			rank=get cache:rank
			if not exists rank
				rank=计算排名..
				MULTI
				set cache:rank rank 
				EXPIRE cache:rank 3600
				EXEC

但是这种方法在很多场合中并不能满足需要。当服务器内存有限时,如果大量的使用缓存键且过期时间设置的很长就会导致Redis占满内存。
另一方面如果为了防止Redis占用内存过大而将过期时间设置的很短,就可能会导致缓存命中率过低。
实际的开发中我们很难为缓存设置一个合适的过期时间,为此我们可以限制Redis能够使用的最大内存,并让Redis按照规则淘汰不需要的缓存键。

具体的设置方法为:修改配置文件的maxmemory参数,限制Redis的最大可用内存大小(单位是字节)。
当超出了这个限制时Redis会根据maxmemory-police参数指定的策略来删除不需要的键,直到Redis占用的内存小于指定的内存。
LRU算法即“最近最少使用”则被删除。
maxmemory-police支持的策略有如下:

在这里插入图片描述
如当设置为allkeys-lru时,Redis会不断的随机删除最近最少使用的键,直到占用的内存小于限定值。

SORT排序

SORT命令可以对列表类型、集合类型和有序集合类型键进行排序

sort key [BY pattern] [LIMIT start count] [GET pattern] [ASC|DESC] [ALPHA] [STORE dstkey] 

我们先来看一个最简单的例子:
在这里插入图片描述
在对有序集合进行排序时会忽略元素的分数,只针对元素自身的值进行排序。
在这里插入图片描述
除了可以对数字进行排序外,还可以通过ALPHA参数实现按照字典顺序排列非数字的元素。
在这里插入图片描述
对于非数字元素,如果不加ALPHA参数,SORT命令会尝试将所有元素转换为双精度浮点数来进行比较,无法转换则会报错。

SORT命令默认是按照从小到大来排序的,可以通过DESC参数指定从大到小排序。
SORT命令还支持LIMIT参数来返回指定范围的结果。LIMIT offset count,表示跳过前offset个元素获取count个元素。
在这里插入图片描述
很多情况下列表(集合、有序集合)中存储的元素值代表的是对象的id,单纯的对这些id进行排序意义不大。
很多时候我们希望根据id对应的对象的某个属性来进行排序。

我们可以通过SORT命令的BY参数来实现。
语法:BY 参考键 参考键可以是字符串类型键或者散列类型键的某个字段。
当参考键为散列类型时:格式为 键名->字段名
当提供了BY参数,SORT命令就不再按照元素自身的值进行排序,而是对每个元素使用元素的值替换参考键中的第一个*,然后根据后面的字段进行排序。

散列类型
在这里插入图片描述
字符串类型
在这里插入图片描述
当参考键名不包含*,即常量键名,SORT将不再执行排序操作,因为无意义,所有要比较的值都一样。
如果几个元素的参考键值相同,则SORT命令会再去比较元素本身的值去决定顺序。
当某个元素的参考键不存在时,会默认参考键的值为0。

参考键虽然支持散列类型,但是*只能在->前面(即键名部分才有用),在->之后会被当成字段名本身而不是占位符,不会被元素替换,即常量键名。

GET参数不影响排序,作用是使SORT命令的返回结果不再是自身的值,而是GET参数中指定的键值。
GET参数的规则和By一样,也支持字符串类型和散列类型的键,并使用*作为占位符。

GET #可以返回元素本身的值。
在这里插入图片描述
默认情况下,SORT命令会直接返回排序结果,如果希望保存排序结果,可以使用STORE参数
保存后的键类型为列表类型,如果键已经存在则会覆盖,加上STORE参数后SORT返回值为结果的个数。
STORE命令经常和EXPIRE命令结合用来缓存排序结果。

在这里插入图片描述

sort性能优化

SORT是Redis最强大最复杂的命令之一,使用不好很容易成为性能瓶颈。
SORT命令的时间复杂度为O(n+mlog(m)),n表示要排序的列表或集合,m表示要返回的元素个数。
当n较大时SORT命令的性能相对较低,并且Redis会在排序前建立一个长度为n的容器来存储待排序的元素,虽然时临时过程,但如果同时进行较多的大数据量排序则会严重影响性能。
所以应该注意
(1)尽量减少待排序键中元素的数量
(2)使用LIMIT参数只获取需要的数据
(3)如果要排序的数据数量较大,尽可能使用STORE参数将结果缓存

使用Redis实现任务队列

网站开发中,当页面需要进行复杂运算、发送邮件等耗时较长的操作时会阻塞页面的渲染。
为了解决这个问题。我们只需要增加一个进程,然后我们只需要通知这个进程进行相应的操作即可。
通知的过程可以通过任务队列来实现,顾名思义,就是传递任务的队列。
与任务队列产生交互的实体有两类,一类是生产者,一类是消费者。
生产者会将需要处理的任务放入任务队列中,消费者则不断从任务队列读取任务并执行。由此可以实现进程间通信。

说到队列很容易的就可以想到Redis的列表类型。
只需要让生产者将任务LPUSH到任务键中,然后让消费者不断从任务键中RPOP取出任务即可。

	伪代码如下:
	
				loop
				task=RPOP queue
				if exists task
					execute(task)
				else
					wait 1 second

当任务队列中没有任务时消费者每秒都会调用一次RPOP,浪费性能。

可以借助BRPOP实现当有新任务加入任务队列时通知消费者。
BRPOP和RPOP类似,区别是列表中没有元素时,BRPOP会一直阻塞住连接,直到有新元素加入。
BRPOP key1 key2… expiretime:该命令两个参数,键名和超时时间。单位为秒。当超过了此时间还没有新元素就会返回nil。0表示不限制等待的时间,如果没有新元素加入就会一直等待下去。
当获得一个元素时会返回两个值,键名和元素值。

我们打开一个Redis-cliA ,输入命令后便堵塞
在这里插入图片描述
我们打开一个Redis-cliB,添加元素后,A会立马接收到并输出
在这里插入图片描述
BLPOP是从队列左边取元素,其他的完全一样。

优先级队列

我们假设这样一种需求,当网站需要在用户发布文章的时候向所有关注的用户发送一封邮件,当用户是刚刚关注的,还要发送一封确认邮件。由于要执行的任务和确认邮件一样,所以二者可以共用一个消费者。假设一个用户拥有一千个粉丝,那么发布一篇文章后就会向任务队列中添加一千个任务,如果一封邮件10s,那么就需要三个小时。但这时有新用户订阅,当他看到需要点击确认邮件时,需要三个小时才能等到确认邮件。而通知邮件并不是很着急,所以应该优先执行确认邮件。所以我们要实现一个优先级队列。

BRPOP可以同时接收多个键,当所有键都没有元素则阻塞。如果其中有一个键有元素,则会从该键中弹出元素。
如果多个键都有元素则按照从左到右的顺序取第一个键中的一个元素。
根据这个特性我们可以实现优先级队列。

例如:
在这里插入图片描述

那么我们就可以用下面的方法解决我们的问题
我们分别创建queue:confirm和 queue:notice两个列表来分别存储确认邮件和通知邮件。

		loop 
		task=BRPOP queue:confirm queue:notice 0
		execute(task)
		
		这样无论notice中还有多少个邮件,都会优先发送确认邮件的任务。

发布/订阅模式

除了实现任务队列外,Redis还提供了一组命令可以让开发者实现发布/订阅(publish/subscribe)模式。
发布/订阅模式同样可以实现进程间的消息传递。
发布/订阅模式中包含两种角色,分别是发布者和订阅者。
订阅者可以订阅一个或若干个频道(channel)。发布者可以向指定的频道发布信息,所有订阅此频道的订阅者都会收到信息。

		PUBLISH channel message:发布信息.返回值表示收到这条信息的订阅者数量。
		SUBSCRIBE channel1 channel2...:订阅一个或多个频道。

(1).我们先发布一条信息,因为此时订阅者为0,所以返回值为0
在这里插入图片描述

(2)接下来我们订阅这个频道
在这里插入图片描述
执行SUBSCRIBE后客户端会进入订阅状态,处于此状态下客户端不能使用除SUBSCRIBE、UNSUBSCRIBE、PSUBSCRIBE、PUNSUBSCRIBE这四个属于发布订阅模式之外的命令,否则会报错。

进入订阅状态后客户端可能收到三种类型的回复,每种类型的回复都包含三个值。
第一个值是消息的类型,根据消息类型的不同,第二、三个值的含义也不同。
消息的类型有以下三种
(1)subscribe。表示订阅成功的反馈信息。第二个值是订阅成功的频道名称。第三个值是当前客户端订阅的频道数量。
(2)message:表示我们收到的信息。第二个值表示产生信息的频道名称。第三个值是消息的内容。
(3)unsubscribe。表示成功取消订阅某个频道。第二个值是对应的频道名称。第三个值是当前客户端订阅的频道数量,当此值为0时客户端会退出订阅状态。

在这里插入图片描述
UNSUBSCRIBE [channel1 channel2…]:取消订阅的频道,如果不指定则取消订阅的所有频道。

按照规则订阅

可以使用PSUBSCRIBE命令订阅指定规则的频道,规则支持glob通配符格式。

		例如:PSUBSCRIBE channel.?*  可以匹配 channel.1 channel.10
		
		返回值有四个
		第一个是pmessage,表示是通过PSUBSCRIBE命令订阅的。
		第二个是channel.?*,表示订阅时使用的通配符。
		第三个是channel.1表示实际收到信息的频道
		第四个是消息内容。

在这里插入图片描述
使用PSUBSCRIBE命令可以重复订阅一个频道,如某客户端执行了PSUBSCRIBE channel.?* channel.?。这时channel.2发布消息后,该客户端会收到两条信息,同时publish返回值也是2而不是1.同样的,如果有客户端执行了
SUBSCRIBE channel.10 和PSUBSCRIBE channel.?的话,channel.10发布消息也会收到两条信息,但是类型不一样,一个是message,一个是pmessage,同时publish返回值也是2。

在这里插入图片描述
PUNSUBSCRIBE [pattern]:退订指定规则的频道,没有参数退订所有。只能退订通过PSUBSCRIBE命令订阅的。

如果给定的频道之前未被订阅过,那么SUBSCRIBE命令会自动创建频道,当频道上没有活跃的订阅者时,频道将会被删除。
发布订阅相关的机制均不支持持久化,也就是说消息、频道和发布者订阅者的关系都不能保存在磁盘上,如果服务器由于某种原因退出,则所有东西都会丢失。

如果频道没有订阅者,那么被发到频道上的信息将被丢弃。
Redis的发布订阅功能并不适合重要信息的投递场景。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值