redis键

场景
  使用Java做过项目的人大概都用过定时器。一般来说,项目里订单模块和评论模块,都会涉及到定时任务执行。比如说:

用户下订单后,需要在5分钟内完成支付,否则订单关闭;
用户在完成订单后,如果没有评论,过一星期,系统自动评论,并完结。
我以前曾经做过一个租车系统,其中订单的预约逻辑是这样的:

用户选择车辆并预约
后台系统开始计时
计时结束后,如果用户没有进行支付,则取消本次订单
 当时后台计时部分的技术,用的就是java中的定时器类Timer ,使用schedule来设置定时任务。虽然说功能实现了,但还是有很多问题,因为Timer本质上还是启动了一个线程来进行处理。当预约用户过多时,系统内存就会飙升,而且当发布新功能时,如果重启服务器,所有的定时器都会丢失。

解决思路
薛定谔解决法
  在订单信息中,加入过期时间,当用户查询订单或其他操作时,检查一下有没有过期的预订单,如果有,则进入逻辑处理。也就是说,当用户不进行操作时,这个预订单是不会自己结束的。这样做的好处在于,当系统重启时,这个订单的状态是不会收到影响的。坏处当然也显而易见,延迟率太高,主动权完全决定在用户手中。

轮询法
  同样的,在订单信息中加入过期时间,后台启动一个定时线程,每隔一段时间遍历一次订单信息,如果有到期的,则结束订单。这种方法同样会影响系统性能。

使用Redis定时器解决
Redis定时器
  Redis中有一个expire命令,用来设置key的过期时间。使用发布订阅,可以接收到key的过期提醒,当key过期时,再执行取消订单的逻辑,就可以了。

redis 定时器演示

127.0.0.1:6379> set test tom EX 10
OK
127.0.0.1:6379> get test
(nil)

设置test(key)的过期时间为10秒,10秒过后key自动销毁。

当然,仅仅有定时器还是不够的,接下来看redis的另一个功能,发布订阅。

Redis发布订阅
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。

Redis 客户端可以订阅任意数量的频道。

用一张图来展示频道(channel1)与订阅者(client2, client5, client1)的关系:
在这里插入图片描述
当有消息发布时,他们的关系:
在这里插入图片描述
消息经由频道广播到每个订阅者中。

发布订阅演示:

首先创建一个频道:

127.0.0.1:6379> SUBSCRIBE chat1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "chat1"
3) (integer) 1

此时控制台进入阻塞状态。

开启2个控制台,分别订阅chat1频道

127.0.0.1:6379> PSUBSCRIBE chat1
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "chat1"
3) (integer) 1

这两个控制台也依次进入阻塞状态。

再开一个控制台,进行信息的发布:

127.0.0.1:6379> PUBLISH chat1 "hello everyone!"
(integer) 3
127.0.0.1:6379>

此时两个订阅者和一个频道创建者都分别收到了相同的内容 hello everyone!

这里放上一张图片,效果可能会比较好:

发消息之前
在这里插入图片描述
发消息之后
在这里插入图片描述
redis的key过期通知也是基于发布订阅模型的。不同的是订阅频道是固定的__keyevent@0__:expired,当然,redis还有好多类似与这种特定频道的通知,想了解更多,可以看这里Redis键空间通知。

Redis过期通知
要使用Redis的过期通知功能,需要首先开启该功能 (2.8.0及以上的版本才有此功能)。

在配置文件中加入如下语句:

notify-keyspace-events Ex

控制台1订阅频道__keyevent@0__:expired

127.0.0.1:6379> PSUBSCRIBE __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)       
1) "psubscribe"                                  
2) "__keyevent@0__:expired"                      
3) (integer) 1

控制台2存入一个key,并设置过期时间

127.0.0.1:6379> set test tom EX 10
OK

当10秒过后,控制台1收到信息

1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "test"

使用springboot接收Redis过期通知
首先在maven中配置redis

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

这个包是spring实现的redis client功能的包。

在springboot配置文件中配置redis

#Redis配置
#数据库索引,默认为0
spring.redis.database=0
#服务器地址,默认localhost
spring.redis.host=localhost
#端口,默认6379
spring.redis.port=6379
#密码,默认为空
spring.redis.password=
#连接池最大连接数,默认为8
spring.redis.jedis.pool.max-active=8
#连接池最大阻塞等待时间,使用负值表示没有限制
spring.redis.jedis.pool.max-wait=-1
#连接池最大空闲连接,默认为8
spring.redis.jedis.pool.max-idle=8
#连接池中的最小空闲连接,默认为0
spring.redis.jedis.pool.min-idle=0
#连接超时时间(毫秒)
spring.redis.timeout=10

代码实现
首先写接收通知的处理方法

@Component
public class RedisMessageReceiver {

    /**
     * 接收redis消息,并处理
     *
     * @param message 过期的redis key
     */
    public void receiveMessage(String message) {
        System.out.println("通知的key是:" + message);
    }

}

再写频道订阅的代码:

@Configuration
public class RedisConfig {

    /**
     * redis 订阅频道
     *
     * @param connectionFactory
     * @param listenerAdapter
     * @return
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 订阅通道,key过期时通知
        container.addMessageListener(listenerAdapter, new PatternTopic("__keyevent@0__:expired"));
        // 可以订阅多个通道

        return container;
    }

    /**
     * 配置redis事件监听处理器
     *
     * @param receiver
     * @return
     */
    @Bean
    MessageListenerAdapter listenerAdapter(RedisMessageReceiver receiver) {
        return new MessageListenerAdapter(receiver, "receiveMessage");
    }

}

这样整个代码就完成了。我们来测试一下效果:

使用控制台,新增一个key,并设置过期时间为10秒

127.0.0.1:6379> set testnofity xxx EX 10
OK
127.0.0.1:6379>

切换到我们的程序中,可以在控制台看到如下信息:
在这里插入图片描述
好了,现在我们就可以根据不同的key做不同的业务逻辑处理了。比如规定,所有订单的key,都必须以order-订单号的形式存入,这样,当接收到订单过期的通知时,就可以解析出订单信息,进一步处理了。
  当然,这个只是一种解决思路,你也可以使用一些其他的方式实现,比如说使用RabbitMQ消息队列实现

命令产生的通知

以下列表记录了不同命令所产生的不同通知

DEL 命令为每个被删除的键产生一个 del 通知。
RENAME 产生两个通知:为来源键(source key)产生一个 rename_from 通知,并为目标键(destination key)产生一个 rename_to 通知。
EXPIRE 和 EXPIREAT 在键被正确设置过期时间时产生一个 expire 通知。当 EXPIREAT 设置的时间已经过期,或者 EXPIRE 传入的时间为负数值时,键被删除,并产生一个 del 通知。
SORT 在命令带有 STORE 参数时产生一个 sortstore 事件。如果 STORE 指示的用于保存排序结果的键已经存在,那么程序还会发送一个 del 事件。
SET 以及它的所有变种(SETEX 、 SETNX 和 GETSET)都产生 set 通知。其中 SETEX 还会产生 expire 通知。
MSET 为每个键产生一个 set 通知。
SETRANGE 产生一个 setrange 通知。
INCR 、 DECR 、 INCRBY 和 DECRBY 都产生 incrby 通知。
INCRBYFLOAT 产生 incrbyfloat 通知。
APPEND 产生 append 通知。
LPUSH 和 LPUSHX 都产生单个 lpush 通知,即使有多个输入元素时,也是如此。
RPUSH 和 RPUSHX 都产生单个 rpush 通知,即使有多个输入元素时,也是如此。
RPOP 产生 rpop 通知。如果被弹出的元素是列表的最后一个元素,那么还会产生一个 del 通知。
LPOP 产生 lpop 通知。如果被弹出的元素是列表的最后一个元素,那么还会产生一个 del 通知。
LINSERT 产生一个 linsert 通知。
LSET 产生一个 lset 通知。
LTRIM 产生一个 ltrim 通知。如果 LTRIM 执行之后,列表键被清空,那么还会产生一个 del 通知。
RPOPLPUSH 和 BRPOPLPUSH 产生一个 rpop 通知,以及一个 lpush 通知。两个命令都会保证 rpop 的通知在 lpush 的通知之前分发。如果从键弹出元素之后,被弹出的列表键被清空,那么还会产生一个 del 通知。
HSET 、 HSETNX 和 HMSET 都只产生一个 hset 通知。
HINCRBY 产生一个 hincrby 通知。
HINCRBYFLOAT 产生一个 hincrbyfloat 通知。
HDEL 产生一个 hdel 通知。如果执行 HDEL 之后,哈希键被清空,那么还会产生一个 del 通知。
SADD 产生一个 sadd 通知,即使有多个输入元素时,也是如此。
SREM 产生一个 srem 通知,如果执行 SREM 之后,集合键被清空,那么还会产生一个 del 通知。
SMOVE 为来源键(source key)产生一个 srem 通知,并为目标键(destination key)产生一个 sadd 事件。
SPOP 产生一个 spop 事件。如果执行 SPOP 之后,集合键被清空,那么还会产生一个 del 通知。
SINTERSTORE 、 SUNIONSTORE 和 SDIFFSTORE 分别产生 sinterstore 、 sunionostore 和 sdiffstore 三种通知。如果用于保存结果的键已经存在,那么还会产生一个 del 通知。
ZINCRBY 产生一个 zincr 通知。(译注:非对称,请注意。)
ZADD 产生一个 zadd 通知,即使有多个输入元素时,也是如此。
ZREM 产生一个 zrem 通知,即使有多个输入元素时,也是如此。如果执行 ZREM 之后,有序集合键被清空,那么还会产生一个 del 通知。
ZREMRANGEBYSCORE 产生一个 zrembyscore 通知。(译注:非对称,请注意。)如果用于保存结果的键已经存在,那么还会产生一个 del 通知。
ZREMRANGEBYRANK 产生一个 zrembyrank 通知。(译注:非对称,请注意。)如果用于保存结果的键已经存在,那么还会产生一个 del 通知。
ZINTERSTORE 和 ZUNIONSTORE 分别产生 zinterstore 和 zunionstore 两种通知。如果用于保存结果的键已经存在,那么还会产生一个 del 通知。
每当一个键因为过期而被删除时,产生一个 expired 通知。
每当一个键因为 maxmemory 政策而被删除以回收内存时,产生一个 evicted 通知

所有命令都只在键真的被改动了之后,才会产生通知。
比如说,当 SREM 试图删除不存在于集合的元素时,删除操作会执行失败,因为没有真正的改动键,所以这一操作不会发送通知
如果对命令所产生的通知有疑问, 最好还是使用以下命令, 自己来验证一下

$ redis-cli config set notify-keyspace-events KEA
$ redis-cli --csv psubscribe '__key*__:*'
Reading messages... (press Ctrl-C to quit)
"psubscribe","__key*__:*",1

然后, 只要在其他终端里用 Redis 客户端发送命令, 就可以看到产生的通知了

"pmessage","__key*__:*","__keyspace@0__:foo","set"
"pmessage","__key*__:*","__keyevent@0__:set","foo"

过期通知的发送时间

Redis 使用以下两种方式删除过期的键:

当一个键被访问时,程序会对这个键进行检查,如果键已经过期,那么该键将被删除。
底层系统会在后台渐进地查找并删除那些过期的键,从而处理那些已经过期、但是不会被访问到的键。
当过期键被以上两个程序的任意一个发现、 并且将键从数据库中删除时, Redis 会产生一个 expired 通知。

Redis 并不保证生存时间(TTL)变为 0 的键会立即被删除: 如果程序没有访问这个过期键, 或者带有生存时间的键非常多的话, 那么在键的生存时间变为 0 , 直到键真正被删除这中间, 可能会有一段比较显著的时间间隔。

因此, Redis 产生 expired 通知的时间为过期键被删除的时候, 而不是键的生存时间变为 0 的时候

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值