中间件之Redis高级特性发布订阅、事务

一、发布订阅

前面说List队列的rpush和blpop可以实现消息队列(队尾进队头出),没有任何元素可以弹出时,连接会被阻塞。

但是基于list实现的消息队列,不支持一对多的消息分发,相当于只有一个消费者

1.订阅频道

消息的生产者和消费者是不同的客户端,连接到同一个Redis服务。通过channe频道l将生产者和消费者关联起来。

订阅者可以订阅一个或多个channel。消息的发布者可以给指定的channel发布消息。当消息进入channel,所有订阅了这个channel的订阅者都会收到这条消息。

redis中的消息队列,订阅者可以订阅多个channel,发布者不能一次向多个channel发送消息。

订阅者(消费者)

# 订阅三个channel
subscribe channel-1 channel-2 channle-3
# 取消订阅
unsubscribe channel-1

发布者(生产者)

publish channel-1 abc

2.按规则(Pattern)订阅频道

redis的消息队列模型中消费者支持?*占位符,?代表一个字符,*代表0个或者多个字符。

例如:现在有三个channel,channel-java、channel-python,channel-javaee

消费者1:psubscribe channel*
消费者2:psubscribe *ee
消费者3:psubscribe *java*

生产者向三个channel发送3条消息,对应的消费者是否能收到?

publish channel-java java
publish channel-python python
publish channel-javaee javaee

结果:

  • 生产者
    在这里插入图片描述

  • 消费者1
    在这里插入图片描述

  • 消费者2
    在这里插入图片描述

  • 消费者3
    在这里插入图片描述

使用redistemplate实现消息队列

配置redistemplate序列化方式

@Configuration
public class RedisConfig {
	@Bean
	public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory)
	{
		RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
		redisTemplate.setConnectionFactory(connectionFactory);
		// 使用Jackson2JsonRedisSerialize替换默认序列化方式
		Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
		StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
		ObjectMapper om = new ObjectMapper();
		om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
		//启用默认的类型
		om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
		//序列化类,对象映射设置
		jackson2JsonRedisSerializer.setObjectMapper(om);
		redisTemplate.setKeySerializer(stringRedisSerializer);
		redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
		redisTemplate.setHashKeySerializer(stringRedisSerializer);
		redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
		redisTemplate.afterPropertiesSet();
		return redisTemplate;
	}
}

消费者

@Configuration
public class RedisMessageListener {
	private static Jackson2JsonRedisSerializer seria;
	static {
		//序列化对象(特别注意:发布的时候需要设置序列化;订阅方也需要设置序列化)
		seria = new Jackson2JsonRedisSerializer(Object.class);
		ObjectMapper objectMapper = new ObjectMapper();
		objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
		objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
		seria.setObjectMapper(objectMapper);
	}
	@Bean
	MessageListenerAdapter messageListener() {
		return new MessageListenerAdapter((MessageListener) (message, bytes) -> {
			System.out.println("接收数据:" + message.toString());
			System.out.println("订阅频道:" + new String(message.getChannel()));

		});
	}
	@Bean
	RedisMessageListenerContainer channel(RedisConnectionFactory factory) {
		final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		container.setConnectionFactory(factory);
		container.addMessageListener(messageListener(), new PatternTopic("channel*"));
		container.setTopicSerializer(seria);
		return container;
	}
	@Bean
	RedisMessageListenerContainer ee(RedisConnectionFactory factory) {
		final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		container.setConnectionFactory(factory);
		container.addMessageListener(messageListener(), new ChannelTopic("channel-javaee"));
		container.setTopicSerializer(seria);
		return container;
	}
	@Bean
	RedisMessageListenerContainer java(RedisConnectionFactory factory) {
		final RedisMessageListenerContainer container = new RedisMessageListenerContainer();
		container.setConnectionFactory(factory);
		container.addMessageListener(messageListener(), new PatternTopic("*java*"));
		container.setTopicSerializer(seria);
		return container;
	}
}

生产者

    @RequestMapping("/publish")
    public void test() {
        String cjava = "channel-java";
        String cpython = "channel-python";
        String cjavaee = "channel-javaee";
        String mjava = "java";
        String mpython = "python";
        String mjavaee = "javaee";
        redisTemplate.convertAndSend(cjava, mjava);
        redisTemplate.convertAndSend(cpython, mpython);
        redisTemplate.convertAndSend(cjavaee, mjavaee);
    }

结果

接收数据:"java"
订阅频道:channel-java
接收数据:"java"
订阅频道:channel-java
接收数据:"python"
订阅频道:channel-python
接收数据:"javaee"
接收数据:"javaee"
订阅频道:channel-javaee
订阅频道:channel-javaee
接收数据:"javaee"
订阅频道:channel-javaee

3.小结

一般来说,考虑到性能和持久化的因素,不建议使用Redis的发布订阅模型来实现MQ。

比如消费者宕机/重启,那么在重启期间发送的消息是不可靠不可以被消费到的。会导致生产者消息丢失。

但是在某些场景下,由程序员自己做控制,也可以使用redis实现订阅消费模型。

Redis的一些内部机制用到了发布订阅功能。

二、Redis事务

因为Redis是单线程队列IO复用机制,所有Redis的单个命令是原子性的,要么成功要么失败,不存在并发干扰的情况。

如果涉及到一次性执行多个命令,且需要把多个命令作为一个不可分割的处理序列,就必须依赖Redis的事务特性来实现了。

Redis事务的特点:

  1. 按照进入队列的顺序执行
  2. 不会收到其他客户端的请求的影响
  3. 事务不能嵌套,嵌套多个事务的命令效果一样,取最外层事务。

1.简单使用

multi:开启事务
exec:执行事务
discard:取消事务
watch:监视

例如:转账场景,我给你转了10块钱。

set me 1000
set you 1000
multi
decrby me 10
incrby you 10
exec
get me
get you

在这里插入图片描述
通过multi的命令开启事务。multi执行后,客户端可以继续向服务器发送多条命令,这些命令放在一个队列中,不会立即执行。当执行exec命令时,所有队列中的命令才会被执行。

放弃执行
如果程序中间出现异常,或者中途不想执行事务。可以调用discard清空事务列表放弃执行。

get me 
multi
decrby me 100
discard
get me

在这里插入图片描述
在RedisTemplate中也封装了使用的方法

/**
 * 直接抛出异常
 */
@RequestMapping("/redisTransaction")
public void redisTransaction() {
    redisTemplate.setEnableTransactionSupport(true);
    SessionCallback sessionCallback = new SessionCallback() {
        @Override
        public Object execute(RedisOperations redisOperations) throws DataAccessException {
            redisOperations.multi();
            ValueOperations<Object, Object> ops = redisOperations.opsForValue();
            ops.set("me", 1000);
            ops.set("you", 1000);
            ops.decrement("me", 10);
            ops.increment("you", 10);
            //模拟异常
            int i = 1 / 0;
            return redisOperations.exec();
        }
    };
    redisTemplate.execute(sessionCallback);
    System.out.println(redisTemplate.opsForValue().get("me"));
    System.out.println(redisTemplate.opsForValue().get("you"));
}

写法2

@RequestMapping("/redisTransaction2")
public void redisTransaction2() {
    // 开启事务支持,在同一个 Connection 中执行命令
    redisTemplate.setEnableTransactionSupport(true);
    redisTemplate.multi();
    ValueOperations<Object, Object> ops = redisTemplate.opsForValue();
    try {
        ops.set("me", 1000);
        ops.set("you", 1000);
        ops.decrement("me", 10);
        ops.increment("you", 10);
        //模拟异常
        int i = 1 / 0;
        List<Object> exec = redisTemplate.exec();
    } catch (Exception e) {
    	//取消执行清空队列
        redisTemplate.discard();
    }
    //正常:me 990 you 1010.  异常 me null you null
    System.out.println(redisTemplate.opsForValue().get("me"));//正常990
    System.out.println(redisTemplate.opsForValue().get("you"));//1010
}

2.watch命令

为了防止事务过程中某个key被其他客户端修改带来非预期的结果,在Redis中还提供了一个watch命令。

也就是多个客户端更新变量的时候,会跟原值作比较,只有它没有被其他线程修改的情况下,才能执行成功。为Redis事务提供了CAS乐观锁(Compare and Swap)

我们可以用watch监视一个或者多个key,如果开启事务之后,至少有一个key在exec执行前被修改了,那么整个事务都会被取消。

set mywatch 1000
watch mywatch
multi 
incrby mywatch 10
exec
get mywatch

在这里插入图片描述
RedisTemplate中操作

/**
 * 开启线程来对watch的key进行操作,看事务是否生效
 * @throws InterruptedException
 */
@RequestMapping("/redisWatch")
public void redisWatch() throws InterruptedException {
    CountDownLatch downLatch = new CountDownLatch(1);
    Thread thread = new Thread(() -> {
        //在线程中对me进行操作
        redisTemplate.opsForValue().set("me", 1);
        downLatch.countDown();
    });
    // 开启事务支持,在同一个 Connection 中执行命令
    redisTemplate.setEnableTransactionSupport(true);
    ValueOperations<Object, Object> ops = redisTemplate.opsForValue();
    redisTemplate.watch("me");
    redisTemplate.multi();
    ops.set("me", 1000);
    ops.set("you", 1000);
    ops.decrement("me", 10);
    ops.increment("you", 10);
    thread.start();
    //等待线程thread执行完毕
    downLatch.await();
    List<Object> exec = redisTemplate.exec();
    //得出结果me:1,you:null。证明事务回滚
    System.out.println(redisTemplate.opsForValue().get("me"));//1
    System.out.println(redisTemplate.opsForValue().get("you"));//null
}

3.执行事务场景

  • 在执行exec之前发生错误
    比如:入队的命令存在语法错误,包括参数数量,参数名等等。此时被事务管理的命令不会执行。
    multi
    set my 100
    hset you 100
    exec
    
  • 在执行exec之后发生错误
    比如对String类型的key使用了Hash的命令,参数个数正确,但数据类型错误。这是一种运行时错误。我们发现set k1 1是成功的。也就是说事务没有进行回滚,第一个命令正常执行。
    127.0.0.1:6379> multi
    OK
    127.0.0.1:6379> set k1 1
    QUEUED
    127.0.0.1:6379> hset k1 b c
    QUEUED
    127.0.0.1:6379> exec
    1) OK
    2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
    127.0.0.1:6379> get k1
    "1"
    127.0.0.1:6379>
    

在exec之后发生的错误显然不符合我们的正常理解。也就是我们没办法用Redis这种事务机制来实现原子性,无法保证数据的一直。

官方解释:

  • Redis命令只会因为错误的语法导致失败,也就是说,从实用性的角度来讲,失败的命令是由代码错误造成的,而这些错误应该在开发过程中被发现,不应该出现在生产环境中
  • 因为不需要对回滚进行支持,所以Redis内部可以保保持简单且快速。需要直到的是:回滚不能解决代码问题。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程大帅气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值