Supposons que nous ayons un tel scénario commercial. Après avoir passé une commande et payé sur le site Web, nous devons informer le service d'inventaire pour le traitement de la livraison.
Il n'est pas difficile de mettre en œuvre les activités ci-dessus. Il nous suffit de fournir le service d'inventaire aux points de vente concernés. Après avoir passé la commande et payé, il nous suffit d'appeler le service d'inventaire.
S'il y a une nouvelle affaire plus tard, comme le service de points, il doit obtenir les résultats du paiement de la commande puis augmenter les points de l'utilisateur.
Ce n'est pas difficile à mettre en œuvre. Le service points propose également une interface. Après avoir passé commande et payé, il vous suffit d'appeler le service inventaire.
S'il n'y a que deux entreprises qui doivent obtenir les résultats du paiement de la commande, alors il est relativement facile de modifier le programme. Cependant, à mesure que l’activité continue de se développer, de plus en plus de nouvelles entreprises doivent passer des commandes et effectuer des paiements.
À l'heure actuelle, nous constaterons qu'il existe de nombreux problèmes dans l'architecture du système, comme ci-dessus :
Premièrement, l'activité de paiement des commandes est fortement couplée à d'autres activités. Chaque fois qu'il y a une nouvelle activité qui nécessite des résultats de paiement, le paiement des commandes. il faut changer les affaires.
Deuxièmement, s'il y a trop d'appels professionnels, le temps de réponse de l'interface de paiement des commandes deviendra plus long. L'une des raisons pour lesquelles la réponse de l'interface de paiement de commande devient plus longue en raison de la synchronisation est que la réponse de l'interface en aval devient plus lente.
Troisièmement, si une interface en aval échoue, une incohérence des données peut en résulter. Par exemple, dans la figure ci-dessous, A est appelé en premier, puis B est appelé après succès et enfin C est appelé.
Si une exception se produit lors de l'appel de l'interface B, cela peut entraîner un échec de retour de l'interface de paiement de la commande. Cependant, à ce stade, l'interface A a en fait été appelée avec succès, ce qui signifie qu'elle a traité le résultat en interne. du paiement réussi de la commande.
Cela conduira à trois interfaces en aval A, B et C. A obtient avec succès le résultat du paiement, mais B et C ne parviennent pas à l'obtenir, ce qui entraîne une incohérence dans les données des trois systèmes.
En fait, si nous y réfléchissons bien, pour l'activité de paiement des commandes, elle n'a en fait pas besoin de se soucier des résultats des appels en aval, tant qu'il existe un mécanisme pour les notifier.
À ce stade, nous devons introduire le mécanisme de publication et d'abonnement qui doit être introduit aujourd'hui.
Redis fournit un mécanisme de messagerie basé sur le modèle « publier/s'abonner ». Dans ce modèle, les éditeurs de messages et les abonnés n'ont pas besoin de communiquer directement.
Comme le montre l'image ci-dessus, l'éditeur du message n'a qu'à publier le message sur la chaîne spécifiée, et chaque client qui s'abonne à la chaîne peut recevoir ce message.
À l'aide du mécanisme de publication et d'abonnement Redis, pour l'activité ci-dessus, l'entreprise de paiement des commandes n'a besoin que d'envoyer des messages au canal Payment Result, et les autres entreprises en aval s'abonnent au canal Payment Result pour recevoir les messages correspondants et puis faites simplement le traitement métier.
De cette manière, la relation d'appel entre l'amont et l'aval du système peut être découplée.
Ensuite, jetons un coup d'œil, voyons comment utiliser la fonction de publication et d'abonnement de Redis.
Redis fournit un ensemble de commandes qui peuvent être utilisées pour publier des messages, s'abonner à des chaînes, se désabonner et s'abonner selon des modèles.
Tout d'abord, voyons comment publier un message. C'est en fait très simple, utilisez simplement la commande publish :
publish channel message
Dans l'image ci-dessus, nous utilisons la commande publish pour envoyer un message. à la chaîne pay_result. Nous pouvons voir que redis nous renvoie 0, ce qui représente en fait le nombre actuel d'abonnés. Puisqu'il n'y a aucun abonnement pour le moment, le résultat renvoyé est 0.
Ensuite, nous utilisons subscribe pour nous abonner à une ou plusieurs chaînes
subscribe channel [channel ...]
Comme le montre l'image ci-dessus, nous nous abonnons à pay_result cette chaîne lorsque d'autres clients envoient des messages à cette chaîne,
.Les abonnés actuels recevront le message.
Lorsque nous utilisons la commande d'abonnement, nous devons faire attention aux points suivants :
Tout d'abord, une fois que le client a exécuté la commande d'abonnement, il entrera dans l'état d'abonnement, et ensuite il ne pourra recevoir que subscribe , psubscribe, unsubscribe , punsubscribe Ces quatre commandes.
第二,新订阅的客户端,是无法收到这个频道之前的消息,这是因为 Redis 并不会对发布的消息持久化的。
相比于很多专业 MQ,比如 kafka、rocketmq 来说, redis 发布订阅功能就显得有点简陋了。如果当前的使用场景可以容忍这些缺点,那么简单优秀的 redis 发布订阅功能值得选择。
除了上面的功能以外的,Redis 还支持模式匹配的订阅方式。简单来说,客户端可以订阅一个带 *
号的模式,如果某些频道的名字与这个模式匹配,那么当其他客户端发送给消息给这些频道时,订阅这个模式的客户端也将会到收到消息。
使用 Redis 订阅模式,我们需要使用一个新的指令 psubscribe。
我们执行下面这个指令:
psubscribe pay.*
那么一旦有其他客户端往 pay 开头的频道,比如 pay_result
、pay_xxx
,我们都可以收到消息。
如果需要取消订阅模式,我们需要使用相应punsubscribe
指令,比如取消上面订阅的模式:
punsubscribe pay.*
聊完 Redis 发布订阅指令,我们来看下 Java Redis 客户端如何使用发布订阅。
下面的例子主要基于 Jedis,maven 版本为:
<dependency> <groupid>redis.clients</groupid> <artifactid>jedis</artifactid> <version>3.1.0</version> </dependency>Copier après la connexion其他 Redis 客户端大同小异。
jedis 发布代码比较简单,只需要调用 Jedis
类的 publish
方法。
// 生产环境千万不要这么使用哦,推荐使用 JedisPool 线程池的方式 Jedis jedis = new Jedis("localhost", 6379); jedis.auth("xxxxx"); jedis.publish("pay_result", "hello world");
订阅的代码就相对复杂了,我们需要继承 JedisPubSub
实现里面的相关方法,一旦有其他客户端往订阅的频道上发送消息,将会调用 JedisPubSub
相应的方法。
private static class MyListener extends JedisPubSub { @Override public void onMessage(String channel, String message) { System.out.println("收到订阅频道:" + channel + " 消息:" + message); } @Override public void onPMessage(String pattern, String channel, String message) { System.out.println("收到具体订阅频道:" + channel + "订阅模式:" + pattern + " 消息:" + message); } }
其次我们需要调用 Jedis
类的 subscribe
方法:
Jedis jedis = new Jedis("localhost", 6379); jedis.auth("xxx"); jedis.subscribe(new MyListener(), "pay_result");
当有其他客户端往 pay_result
频道发送消息时,订阅将会收到消息。
不过需要注意的是,jedis#subscribe
是一个阻塞方法,调用之后将会阻塞主线程的,所以如果需要在正式项目使用需要使用异步线程运行,这里就不演示具体的代码了。
原生 jedis 发布订阅操作,相对来说还是有点复杂。现在我们很多应用已经基于 SpringBoot 开发,使用 spring-boot-starter-data-redis
,可以简化发布订阅开发。
首先我们需要引入相应的 startter 依赖:
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-data-redis</artifactid> <exclusions> <exclusion> <artifactid>lettuce-core</artifactid> <groupid>io.lettuce</groupid> </exclusion> </exclusions> </dependency> <dependency> <groupid>redis.clients</groupid> <artifactid>jedis</artifactid> </dependency>
这里我们使用 Jedis 当做底层连接客户端,所以需要排除 lettuce,然后引入 Jedis 依赖。
然后我们需要创建一个消息接收类,里面需要有方法消费消息:
@Slf4j public class Receiver { private AtomicInteger counter = new AtomicInteger(); public void receiveMessage(String message) { log.info("Received "); counter.incrementAndGet(); } public int getCount() { return counter.get(); } }
接着我们只需要注入 Spring- Redis 相关 Bean,比如:
StringRedisTemplate
,用来操作 Redis 命令
MessageListenerAdapter
,消息监听器,可以在这个类注入我们上面创建消息接受类 Receiver
RedisConnectionFactory
, 创建 Redis 底层连接
@Configuration public class MessageConfiguration { @Bean RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); // 订阅指定频道使用 ChannelTopic // 订阅模式使用 PatternTopic container.addMessageListener(listenerAdapter, new ChannelTopic("pay_result")); return container; } @Bean MessageListenerAdapter listenerAdapter(Receiver receiver) { // 注入 Receiver,指定类中的接受方法 return new MessageListenerAdapter(receiver, "receiveMessage"); } @Bean Receiver receiver() { return new Receiver(); } @Bean StringRedisTemplate template(RedisConnectionFactory connectionFactory) { return new StringRedisTemplate(connectionFactory); } }
最后我们使用 StringRedisTemplate#convertAndSend
发送消息,同时 Receiver
将会收到一条消息。
@SpringBootApplication public class MessagingRedisApplication { public static void main(String[] args) throws InterruptedException { ApplicationContext ctx = SpringApplication.run(MessagingRedisApplication.class, args); StringRedisTemplate template = ctx.getBean(StringRedisTemplate.class); Receiver receiver = ctx.getBean(Receiver.class); while (receiver.getCount() == 0) { template.convertAndSend("pay_result", "Hello from Redis!"); Thread.sleep(500L); } System.exit(0); } }
Redis Sentinel 是 Redis 一套高可用方案,可以在主节点故障的时候,自动将从节点提升为主节点,从而转移故障。
今天这里我们不详细解释 Redis Sentinel 详细原理,主要来看下 Redis Sentinel 如何使用发布订阅机制。
Redis Sentinel 节点主要使用发布订阅机制,实现新节点的发现,以及交换主节点的之间的状态。
如下所示,每一个 Sentinel 节点将会定时向 _sentinel_:hello
频道发送消息,并且每个 Sentinel 都会订阅这个节点。
这样一旦有节点往这个频道发送消息,其他节点就可以立刻收到消息。
这样一旦有的新节点加入,它往这个频道发送消息,其他节点收到之后,判断本地列表并没有这个节点,于是就可以当做新的节点加入本地节点列表。
除此之外,每次往这个频道发送消息内容可以包含节点的状态信息,这样可以作为后面 Sentinel 领导者选举的依据。
以上都是对于 Redis 服务端来讲,对于客户端来讲,我们也可以用到发布订阅机制。
当 Redis Sentinel 进行主节点故障转移,这个过程各个阶段会通过发布订阅对外提供。
对于我们客户端来讲,比较关心切换之后的主节点,这样我们及时切换主节点的连接(旧节点此时已故障,不能再接受操作指令),
客户端可以订阅 +switch-master
频道,一旦 Redis Sentinel 结束了对主节点的故障转移就会发布主节点的的消息。
redission 开源框架提供一些便捷操作 Redis 的方法,其中比较出名的 redission 基于 Redis 的实现分布式锁。
今天我们来看下 Redis 的实现分布式锁中如何使用 Redis 发布订阅机制,提高加锁的性能。
PS:redission 分布式锁实现原理,可以参考之前写过的文章:
可重入分布式锁的实现方式
Redis 分布式锁,看似简单,其实真不简单
首先我们来看下 redission 加锁的方法:
Redisson redisson = .... RLock redissonLock = redisson.getLock("xxxx"); redissonLock.lock();
RLock
继承自 Java 标准的 Lock
接口,调用 lock
方法,如果当前锁已被其他客户端获取,那么当前加锁的线程将会被阻塞,直到其他客户端释放这把锁。
这里其实有个问题,当前阻塞的线程如何感知分布式锁已被释放呢?
这里其实有两种实现方法:
第一钟,定时查询分布时锁的状态,一旦查到锁已被释放(Redis 中不存在这个键值),那么就去加锁。
实现伪码如下:
while (true) { boolean result=lock(); if (!result) { Thread.sleep(N); } }
这种方式实现起来起来简单,不过缺点也比较多。
如果定时任务时间过短,将会导致查询次数过多,其实这些都是无效查询。
如果定时任务休眠时间过长,那又会导致加锁时间过长,导致加锁性能不好。
那么第二种实现方案,就是采用服务通知的机制,当分布式锁被释放之后,客户端可以收到锁释放的消息,然后第一时间再去加锁。
这个服务通知的机制我们可以使用 Redis 发布订阅模式。
当线程加锁失败之后,线程将会订阅 redisson_lock__channel_xxx
(xx 代表锁的名称) 频道,使用异步线程监听消息,然后利用 Java 中 Semaphore
使当前线程进入阻塞。
一旦其他客户端进行解锁,redission 就会往这个redisson_lock__channel_xxx
发送解锁消息。
等异步线程收到消息,将会调用 Semaphore
释放信号量,从而让当前被阻塞的线程唤醒去加锁。
ps:这里只是简单描述了 redission 加锁部分原理,出于篇幅,这里就不再消息解析源码。
感兴趣的小伙伴可以自己看下 redission 加锁的源码。
通过发布订阅机制,被阻塞的线程可以及时被唤醒,减少无效的空转的查询,有效的提高的加锁的效率。
ps: 这种方式,性能确实提高,但是实现起来的复杂度也很高,这部分源码有点东西,快看晕了。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!