一、发布订阅
前面说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.简单使用
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内部可以保保持简单且快速。需要直到的是:回滚不能解决代码问题。