需求

需要实现一个超时通知的功能,又不想定时任务扫,所以就百度百度发现了这个,Redis的过期监听事件,就是向Redis里存放一条记录,把需要的信息存在key中,设置过期事件,一旦过期,就触发事件.

实现

要实现这个功能,需要先修改Redis的配置文件.

打开.conf文件,找到notify-keyspace-events这个值,默认值为” “,将其修改为notify-keyspace-events Ex

也可以通过命令设置,执行如下

//修改
config set notify-keyspace-events "Ex"
//查看
config get notify-keyspace-events

参数解析

字符 发送通知
K 键空间通知,所有通知以 keyspace@ 为前缀,针对Key
E 键事件通知,所有通知以 keyevent@ 为前缀,针对event
g DEL 、 EXPIRE 、 RENAME 等类型无关的通用命令的通知
$ 字符串命令的通知
l 列表命令的通知
s 集合命令的通知
h 哈希命令的通知
z 有序集合命令的通知
x 过期事件:每当有过期键被删除时发送
e 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送
A 参数 g$lshzxe 的别名,相当于是All

输入的参数中至少需要一个 K 或者 E,不然不会触发任何通知

斜体 表示通用的操作或者事件

粗体 表示特定数据类型的操作

事件类型

对于每个修改数据库的操作,键空间通知都会发送两种不同类型的事件消息: keyspacekeyevent.

keyspace为前缀的频道被称为 键空间通知key-space notification

keyevent为前缀的频道被称为 键事件通知key-event notification

事件通知是以 __keyspace@DB __: KeyPattern__keyevent@DB__:OpsType 的格式来发送信息的.

DB表示第几个数据库

KeyPattern表示需要监控的键模式,可以用通配符.

OpsType表示操作类型

所以,如果想订阅特殊的Key上的事件,应该订阅Keyspace

所以执行对0号数据库的myKey键执行DEL操作时,系统将发布两条消息, 相当于执行以下两个 publish 命令:

PUBLISH __keyspace@0__:myKey del
PUBLISH __keyevent@0__:del  myKey

订阅第一个频道 __keyspace@0__:myKey 可以接收0号数据库所有修改myKey的事件

订阅第二个频道 __keyevent@0__:del 可以接收0号数据库所有执行del命令的键

订阅

使用subscribe psubscribe命令来对特点主题进行订阅,完成通知的过程

SUBSCRIBE channel [channel ...]
PSUBSCRIBE channelPattern [channelPattern ...]

PSUBSCRIBE对比SUBSCRIBE唯一不同就是支持通配符,每个模式以 * 作为匹配符,比如 huangz* 匹配所有以 huangz.显然支持通配符的性能消耗会大一点.

命令实践

1:订阅

    
subscribe __keyspace@0__:cool
Reading messages... (press Ctrl-C to quit)
1) "subscribe"                   # 返回值的类型:显示订阅成功
2) "__keyspace@0__:cool"         # 订阅的Channel名
3) (integer) 1                   # 目前已订阅的频道数量

2:设置失效

  
setex cool 1 val                 # cool=val 1秒失效

3:失效消息

1)"message"                      # 返回值的类型:信息
2)__keyspace@0__:cool”           # 来源(从哪个Channel发送过来)
3) “expired"                     # 信息内容

注:对于psubscribe,消息会多一行

1) “pmessage”                     # 返回值的类型:信息
2) "__keyspace@0__:cool*”         # 来源(从哪个ChannelPattern发送过来)
3) "__keyspace@0__:cool"          # 实际的Channel
4) “expired"                      # 信息内容

Spring实现

最主要的是KeyExpirationEventMessageListener类,此类是对MessageListener接口的实现

看源码,它其实就是监听的一个__keyevent@*__:expired事件

public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements ApplicationEventPublisherAware {
    //看这里-----------------------------------------------------------------------↓↓↓↓↓↓
    private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");
    @Nullable
    private ApplicationEventPublisher publisher;

    public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    protected void doRegister(RedisMessageListenerContainer listenerContainer) {
        listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
    }

    protected void doHandleMessage(Message message) {
        this.publishEvent(new RedisKeyExpiredEvent(message.getBody()));
    }

    protected void publishEvent(RedisKeyExpiredEvent event) {
        if (this.publisher != null) {
            this.publisher.publishEvent(event);
        }

    }

    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.publisher = applicationEventPublisher;
    }
}

我们只需要继承它,并重写onMessage即可

@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
    public RedisKeyExpirationListener(RedisMessageListenerContainer container) {
        super(container);
    }
  
    /**
     * 针对redis数据失效事件,进行数据处理
     * @param message
     * @param pattern
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
       String key=message.toString();//生效的key
        if (key!=null && key.startsWith("order")){//从失效key中筛选代表订单失效的key
            //截取订单号,查询订单,如果是未支付状态则取消订单
            String orderNo=key.substring(5);
            System.out.println("订单号为:"+orderNo+"的订单超时未支付,取消订单");
  
        }
    }
}

然后在RedisConfig中配置如下

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);

    return container;
}

我们只需把过期事件设置为48小时,就实现了我们的需求

缺陷

  1. 只能获取到失效的key,但是此时不能根据key获取value值,因为该事件是在数据失效后才触发

    有个简单的解决办法,就是一份数据存两份
    比如你现在需要set一份数据:key:value
    可以额外再set一份value相同但key有指定规则的数据:key_copy:value
    第二份数据过期时间相对第一份数据稍微长一点
    这样过期事件执行时可以拿着key根据指定的规则拼装出第二份数据的key,从而得到想要的value
    缺点就是当你需要set的地方比较多时维护起来就非常恶心

  2. Redis的过期事件可能有点不准时,这是因为Redis本身的机制问题

    Redis有一下两种删除方式

    1. 当键被访问时,程序会对键进行一次检查,如果以过期,那么键将被删除
    2. 底层系统会在后台随机查找并删除过期的键,从而处理这些已经过期,但未被查找的键

    当键被以上两个程序任意一个发现并删除时,Redis会产生一个expired通知

    Redis并不保证生存时间变为0时会被立即删除,

    如果程序没有访问这个键,或者带有过期事件的键非常多的话,那么在键的生存时间变为0时,直到真正删除这些键,会存在一个时间差,越多越明显.

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

参考

文章1:https://my.oschina.net/u/182501/blog/1927210

文章2:https://blog.csdn.net/qijiqiguai/article/details/78229111

文章3:http://redisdoc.com/topic/notification.html

文章4:https://segmentfault.com/a/1190000022735302

文章5:https://blog.csdn.net/for_the_time_begin/article/details/90376873