Redis基本使用三(流水线与发布订阅以及超时命令)

开发环境:

  1. JDK11;
  2. Redis3.2;

使用流水线优化读写新能:

与传统关系型数据库类似,每次客户端与服务端建立连接是需要开销的,对于每一个客户端而言,将多个操作封装在一次连接内是十分有必要的,从而产生了Redis的流水线操作,应用步骤如下:

  1. 开启流水线;
  2. 加入命令;
  3. 执行流水线;

程序如下:

    /**
     * 10619ms
     * 普通执行
     */
    private static void test3()
    {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++)
        {
            // 写
            jedis.set("key_" + i, "value_" + i);
            // 读
            jedis.get("key_" + i);
        }
        long end = System.currentTimeMillis();
        System.out.println("普通耗时:" + (end - start));
    }

    /**
     * 630ms
     * 流水线执行
     */
    private static void test4()
    {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        long start = System.currentTimeMillis();
        // 开启流水线
        Pipeline pipeline=jedis.pipelined();
        for (int i = 0; i < 100000; i++)
        {
            // 写
            pipeline.set("key_" + i, "value_" + i);
            // 读
            pipeline.get("key_" + i);
        }
        // 执行流水线,但不返回读的结果,syncAndReturnAll方法会返回结果
        pipeline.sync();
        long end = System.currentTimeMillis();
        System.out.println("流水线耗时:" + (end - start));
    }

 

Redis的发布订阅:

刚开始学的时候笔者也是十分懵,这个发布订阅的作用不如流水线、锁机制等来的明显,其实捋一捋还是有点眉目的,Redis发布订阅无非就是通过Redis提供的渠道将消息发送到这个渠道上,而通过多个系统去监听这个渠道就可以获取消息。

典型应用的一个例子就是移动支付,当你通过微信或支付宝来消费时,就会把消费信息发布到该渠道,此时短信、微信或者支付宝都在监听,获取到消息后银行会发送短信,而微信或支付宝也会有一定的消费信息。

这非常类似于JMS(Java Message Service),其最大的优势笔者认为是作用于不同系统和异步调用。

应用步骤:

  1. 定义消息监听器;
  2. 创建消息监听配置类,定义任务池和消息监听容器;
  3. 发送消息;

定义消息监听器程序如下:

package xyz.lsm1998.listener;

import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

/**
 * 作者:刘时明
 * 日期:2018/11/20
 * 时间:12:52
 * 说明:消息监听器,作用是处理接受到的消息
 */
@Component
public class RedisMessageListener implements MessageListener
{
    @Override
    public void onMessage(Message message, byte[] pattern)
    {
        System.err.println("获取到消息");
        // 获取消息体
        String body = new String(message.getBody());
        // 获取渠道名称
        String topic = new String(pattern);
        // 调用业务
        message(topic, body);
        weChat(topic, body);
    }

    /**
     * 模拟短信发送业务
     * @param topic
     * @param body
     */
    private void message(String topic, String body)
    {
        System.out.println("短信提示:" + body + ",消息来自=" + topic);
    }

    /**
     * 模拟微信发送业务
     * @param topic
     * @param body
     */
    private void weChat(String topic, String body)
    {
        System.out.println("微信提示:" + body + ",消息来自=" + topic);
    }
}

创建消息监听配置类程序如下:

package xyz.lsm1998.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

/**
 * 作者:刘时明
 * 日期:2018/11/20
 * 时间:12:58
 * 说明:
 */
@Configuration
public class RedisMessageConfig
{
    @Autowired
    private RedisConnectionFactory factory;
    @Autowired
    private MessageListener listener;
    @Autowired
    private ThreadPoolTaskScheduler taskScheduler;

    /**
     * 任务池,等待处理Redis的消息
     *
     * @return
     */
    @Bean
    public ThreadPoolTaskScheduler initTaskScheduler()
    {
        if (taskScheduler != null)
            return taskScheduler;
        taskScheduler = new ThreadPoolTaskScheduler();
        taskScheduler.setPoolSize(20);
        return taskScheduler;
    }

    /**
     * 消息监听容器
     *
     * @return
     */
    @Bean
    public RedisMessageListenerContainer initRedisContainer()
    {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        // Redis连接工厂
        container.setConnectionFactory(factory);
        // 设置运行任务池
        container.setTaskExecutor(initTaskScheduler());
        // 定义监听渠道,名称为myTopic
        Topic topic = new ChannelTopic("myTopic");
        // 使用监听器监听Redis的消息
        container.addMessageListener(listener, topic);
        return container;
    }
}

控制器发送消息程序如下:

package xyz.lsm1998.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 作者:刘时明
 * 日期:2018/11/20
 * 时间:14:36
 * 说明:消息发送控制器
 */
@RestController
@RequestMapping("msg")
public class MessageController
{
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 发送消息
     * @return
     */
    @GetMapping("test1")
    public Object test1()
    {
        redisTemplate.convertAndSend("myTopic", "你已经消费了100万人民币");
        return "消息已经成功发送";
    }
}

最后结果亲测可用。

 

超时命令:

由于Redis是基于内存的,所以空间占用非常宝贵,类似于Java的gc机制,当运行内存占用过高时,Redis会自动回收一些键值对,至于具体如何回收?回收哪些键值对?笔者目前也不太清除,但可以肯定的是Redis进行自动垃圾回收时极大可能引发系统停顿,因此使用超时命令可以有效提高系统的性能。

程序如下:

package xyz.lsm1998;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import redis.clients.jedis.Jedis;
import xyz.lsm1998.config.RedisConfig;

import java.util.concurrent.TimeUnit;

/**
 * 作者:刘时明
 * 日期:2018/11/29
 * 时间:22:46
 * 说明:
 */
public class OutTimeTest
{
    public static void main(String[] args)
    {
        var context = new AnnotationConfigApplicationContext(RedisConfig.class);
        RedisTemplate template = context.getBean(RedisTemplate.class);
        outTimeByJedis();
        outTimeByRedisTemplate(template);
    }

    /**
     * Jedis实现超时命令
     */
    private static void outTimeByJedis()
    {
        Jedis jedis = new Jedis("127.0.0.1", 6379);
        String key = "key_1";
        jedis.set(key, "value");
        printOutTime1(jedis, key);
        // 设置超时,秒为单位
        jedis.expire(key, 100);
        printOutTime1(jedis, key);
        // 设置超时,毫秒为单位
        jedis.pexpire(key, 5000L);
        printOutTime1(jedis, key);
    }

    /**
     * RedisTemplate实现超时命令
     *
     * @param template
     */
    private static void outTimeByRedisTemplate(RedisTemplate template)
    {
        ValueOperations operations = template.opsForValue();
        String key = "key_1";
        operations.set(key, "value");
        printOutTime2(operations, key);
        // 设置10秒超时
        operations.getOperations().expire(key, 5L, TimeUnit.SECONDS);
        printOutTime2(operations, key);
        try
        {
            Thread.sleep(5 * 1000);
        }catch (Exception e)
        {
            e.printStackTrace();
        }
        System.out.println("线程休眠5秒");
        printOutTime2(operations, key);
        // 等待key超时,再取出该键值对
        System.out.println("超时后的value="+operations.get(key));
    }

    private static void printOutTime1(Jedis jedis, String key)
    {
        // -1代表不会超时,-2代表key不存在或者已经超时
        System.out.println(key + "的超时时间=" + jedis.ttl(key));
    }

    private static void printOutTime2(ValueOperations operations, String key)
    {
        // -1代表不会超时,-2代表key不存在或者已经超时
        RedisOperations redisOperations = operations.getOperations();
        long time = redisOperations.getExpire(key);
        System.out.println(key + "的超时时间=" + time);
    }
}

PS:Redis的key超时不会被自动回收,而是被标识为超时,但此时程序显示键值对已经为null了,具体原因请看如下的惰性回收。

 原因:虽然这样做会浪费一些内存空间,但对于一个很大的键值对来说,超时回收会产生系统停顿。

Redis有两种方式回收超时键值对:

  1. 定时回收:某个时间触发一次回收代码去回收键值对,当然,何时触发笔者暂时也不明白;
  2. 惰性回收:当一个超时的键再次被get命令访问时将触发Redis回收代码;

你可能感兴趣的:(JavaEE)