草庐IT

RabbitMQ解决消息丢失

Kera_QY 2023-12-21 原文

目录

1.开启发布确认模式

1.1单个确认发布

1.2批量确认发布

1.3异步确认发布

1.4处理异步未确认的消息

1.5 三种发布方式对比

1.6发布确认高级

 2.消息持久化

2.1队列持久化

2.2消息持久化

 3.消费手动确认

单次消息确认

批量消息确认 

确认消息失败重新入队


        上次我们说过,对于解决消息中间件的问题,都是通过三阶段来保证消息不丢失问题。消息的发送阶段+消息的存储阶段+消息的消费阶段。这次我们来说说RabbitMQ怎样操作来保证消息不丢失。消息的发送阶段:ack机制。生产方将消息投递到broker中,需要等待broker的ack确认和nack。当返回ack,可知消息已经投递匹配的队列。否则失败。

1.开启发布确认模式

        生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式,所有在该信道上面发布的 消息都将会被指派一个唯一的 ID (从 1 开始),一旦消息被投递到所有匹配的队列之后,broker 就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

1.1单个确认发布

发布一个消息之后只有它 被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。这种确认方式有一个最大的缺点就是:发布速度特别的慢,因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应 用程序来说这可能已经足够了。
public static void publishMessageIndividually() throws Exception {
 try (Channel channel = RabbitMqUtils.getChannel()) {
     String queueName = UUID.randomUUID().toString();
     channel.queueDeclare(queueName, false, false, false, null);
     //开启发布确认
     channel.confirmSelect();
     long begin = System.currentTimeMillis();
     for (int i = 0; i < MESSAGE_COUNT; i++) {
     String message = i + "";
     channel.basicPublish("", queueName, null, message.getBytes());
     //服务端返回 false 或超时时间内未返回,生产者可以消息重发
     boolean flag = channel.waitForConfirms();
     if(flag){
     System.out.println("消息发送成功");
     }
   }
     long end = System.currentTimeMillis();
     System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - begin) + 
    "ms");
  }
}

1.2批量确认发布

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地
提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时,不知道是哪个消息出现问题了,我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。
public static void publishMessageBatch() throws Exception {
 try (Channel channel = RabbitMqUtils.getChannel()) {
     String queueName = UUID.randomUUID().toString();
     channel.queueDeclare(queueName, false, false, false, null);
     //开启发布确认
     channel.confirmSelect();
     //批量确认消息大小
     int batchSize = 100;
     //未确认消息个数
     int outstandingMessageCount = 0;
     long begin = System.currentTimeMillis();
     for (int i = 0; i < MESSAGE_COUNT; i++) {
     String message = i + "";
     channel.basicPublish("", queueName, null, message.getBytes());
     outstandingMessageCount++;
     if (outstandingMessageCount == batchSize) {
     channel.waitForConfirms();
     outstandingMessageCount = 0;
     }
   }
     //为了确保还有剩余没有确认消息 再次确认
     if (outstandingMessageCount > 0) {
     channel.waitForConfirms();
    }
     long end = System.currentTimeMillis();
     System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - begin) + 
"ms");
   }
}

1.3异步确认发布

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。
异步确认发布原理图

 代码

public static void publishMessageAsync() throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
  String queueName = UUID.randomUUID().toString();
  channel.queueDeclare(queueName, false, false, false, null);
  //开启发布确认
  channel.confirmSelect();
  /**
  * 线程安全有序的一个哈希表,适用于高并发的情况
  * 1.轻松的将序号与消息进行关联
  * 2.轻松批量删除条目 只要给到序列号
  * 3.支持并发访问
  */
  ConcurrentSkipListMap<Long, String> outstandingConfirms = new 
  ConcurrentSkipListMap<>();
  /**
  * 确认收到消息的一个回调
  * 1.消息序列号
  * 2.true 可以确认小于等于当前序列号的消息
  * false 确认当前序列号消息
  */
  ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
  if (multiple) {
  //返回的是小于等于当前序列号的未确认消息 是一个 map
  ConcurrentNavigableMap<Long, String> confirmed = 
  outstandingConfirms.headMap(sequenceNumber, true);
  //清除该部分未确认消息
  confirmed.clear();
  }else{
  //只清除当前序列号的消息
  outstandingConfirms.remove(sequenceNumber);
  }
 };
  ConfirmCallback nackCallback = (sequenceNumber, multiple) -> {
  String message = outstandingConfirms.get(sequenceNumber);
  System.out.println("发布的消息"+message+"未被确认,序列号"+sequenceNumber);
  };
  /**
  * 添加一个异步确认的监听器
  * 1.确认收到消息的回调
  * 2.未收到消息的回调
  */
  channel.addConfirmListener(ackCallback, null);
  long begin = System.currentTimeMillis();
  for (int i = 0; i < MESSAGE_COUNT; i++) {
  String message = "消息" + i;
  /**
  * channel.getNextPublishSeqNo()获取下一个消息的序列号
  * 通过序列号与消息体进行一个关联
  * 全部都是未确认的消息体
  */
  outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
  channel.basicPublish("", queueName, null, message.getBytes());
  }
  long end = System.currentTimeMillis();
  System.out.println("发布" + MESSAGE_COUNT + "个异步确认消息,耗时" + (end - begin) + 
 "ms");
  }
}

1.4处理异步未确认的消息

最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,
比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

1.5 三种发布方式对比

单独发布消息:同步等待确认,简单,但吞吐量非常有限。
批量发布消息:批量同步等待确认,简单,合理的吞吐量,一旦出现问题但很难推断出是那条消息出现了问题。
异步处理: 最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些。

1.6发布确认高级

        在生产环境中由于一些不明原因,导致 rabbitmq 重启,在 RabbitMQ 重启期间生产者消息投递失败,导致消息丢失,需要手动处理和恢复。于是,我们开始思考,如何才能进行 RabbitMQ 的消息可靠投递呢?特别是在这样比较极端的情况,RabbitMQ 集群不可用的时候,无法投递的消息该如何处理呢。我们需要知道消息是否成功发送到broker。这和我们上面说到的发布确认不一样,上面是消费已经成功发送到broker后的回调。现在是需要知道消息发送到broker的回调。
步骤:
配置文件
在配置文件当中需要添加
spring.rabbitmq.publisher-confirm-type=correlated
  NONE
禁用发布确认模式,是默认值
CORRELATED
发布消息成功到交换器后会触发回调方法
SIMPLE

经测试有两种效果,其一效果和 CORRELATED 值一样会触发回调方法,其二在发布消息成功后使用 rabbitTemplate 调用 waitForConfirms waitForConfirmsOrDie 方法等待 broker 节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点waitForConfirmsOrDie 方法如果返回 false 则会关闭 channel ,则接下来无法发送消息到 broker。

添加配置类

@Configuration
public class ConfirmConfig {
 public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
 public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
 //声明业务 Exchange
 @Bean("confirmExchange")
 public DirectExchange confirmExchange(){
 return new DirectExchange(CONFIRM_EXCHANGE_NAME);
 }
 // 声明确认队列
 @Bean("confirmQueue")
 public Queue confirmQueue(){
 return QueueBuilder.durable(CONFIRM_QUEUE_NAME).build();
 }
 // 声明确认队列绑定关系
 @Bean
 public Binding queueBinding(@Qualifier("confirmQueue") Queue queue,
 @Qualifier("confirmExchange") DirectExchange exchange){
 return BindingBuilder.bind(queue).to(exchange).with("key1");
 }
}

消息生产者

@RestController
@RequestMapping("/confirm")
@Slf4j
public class Producer {
 public static final String CONFIRM_EXCHANGE_NAME = "confirm.exchange";
 @Autowired
 private RabbitTemplate rabbitTemplate;
 @Autowired
 private MyCallBack myCallBack;
 //依赖注入 rabbitTemplate 之后再设置它的回调对象
 @PostConstruct
 public void init(){
 rabbitTemplate.setConfirmCallback(myCallBack);
 }
 @GetMapping("sendMessage/{message}")
 public void sendMessage(@PathVariable String message){
 //指定消息 id 为 1
 CorrelationData correlationData1=new CorrelationData("1");
 String routingKey="key1";
 
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correl
ationData1);
 CorrelationData correlationData2=new CorrelationData("2");
 routingKey="key2";
 
rabbitTemplate.convertAndSend(CONFIRM_EXCHANGE_NAME,routingKey,message+routingKey,correl
ationData2);
 log.info("发送消息内容:{}",message);
 }
}

回调接口

@Component
@Slf4j
public class MyCallBack implements RabbitTemplate.ConfirmCallback {
 /**
 * 交换机不管是否收到消息的一个回调方法
 * CorrelationData
 * 消息相关数据
 * ack
 * 交换机是否收到消息
 */
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 String id=correlationData!=null?correlationData.getId():"";
 if(ack){
 log.info("交换机已经收到 id 为:{}的消息",id);
 }else{
 log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
 }
 }
}

 消息消费者

@Component
@Slf4j
public class ConfirmConsumer {
 public static final String CONFIRM_QUEUE_NAME = "confirm.queue";
 @RabbitListener(queues =CONFIRM_QUEUE_NAME)
 public void receiveMsg(Message message){
 String msg=new String(message.getBody());
 log.info("接受到队列 confirm.queue 消息:{}",msg);
 }
}

 回退消息

在仅开启了生产者确认机制的情况下,交换机接收到消息后,会直接给消息生产者发送确认消息 果发现该消息不可路由,那么消息会被直接丢弃,此时生产者是不知道消息被丢弃这个事件的 。那么如何让无法被路由的消息帮我想办法处理一下?最起码通知我一声,我好自己处理啊。通过设置 mandatory 参数可以在当消息传递过程中不可达目的地时将消息返回给生产者。

生产者

@Slf4j
@Component
public class MessageProducer implements RabbitTemplate.ConfirmCallback , 
RabbitTemplate.ReturnCallback {
 @Autowired
 private RabbitTemplate rabbitTemplate;
 //rabbitTemplate 注入之后就设置该值
 @PostConstruct
 private void init() {
 rabbitTemplate.setConfirmCallback(this);
 /**
 * true:
 * 交换机无法将消息进行路由时,会将该消息返回给生产者
 * false:
 * 如果发现消息无法进行路由,则直接丢弃
 */
 rabbitTemplate.setMandatory(true);
 //设置回退消息交给谁处理
 rabbitTemplate.setReturnCallback(this);
 }
 @GetMapping("sendMessage")
public void sendMessage(String message){
 //让消息绑定一个 id 值
 CorrelationData correlationData1 = new CorrelationData(UUID.randomUUID().toString());
 
rabbitTemplate.convertAndSend("confirm.exchange","key1",message+"key1",correlationData1)
;
 log.info("发送消息 id 为:{}内容为{}",correlationData1.getId(),message+"key1");
 CorrelationData correlationData2 = new CorrelationData(UUID.randomUUID().toString());
 
rabbitTemplate.convertAndSend("confirm.exchange","key2",message+"key2",correlationData2)
;
 log.info("发送消息 id 为:{}内容为{}",correlationData2.getId(),message+"key2");
}
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 String id = correlationData != null ? correlationData.getId() : "";
 if (ack) {
 log.info("交换机收到消息确认成功, id:{}", id);
 } else {
 log.error("消息 id:{}未成功投递到交换机,原因是:{}", id, cause);
 }
 }
 @Override
 public void returnedMessage(Message message, int replyCode, String replyText, String 
exchange, String routingKey) {
 log.info("消息:{}被服务器退回,退回原因:{}, 交换机是:{}, 路由 key:{}",
 new String(message.getBody()),replyText, exchange, routingKey);
 }
}

 回调接口

@Component
@Slf4j
public class MyCallBack implements 
RabbitTemplate.ConfirmCallback,RabbitTemplate.ReturnCallback {
 /**
 * 交换机不管是否收到消息的一个回调方法
 * CorrelationData
 * 消息相关数据
 * ack
 * 交换机是否收到消息
 */
 @Override
 public void confirm(CorrelationData correlationData, boolean ack, String cause) {
 String id=correlationData!=null?correlationData.getId():"";
 if(ack){
 log.info("交换机已经收到 id 为:{}的消息",id);
 }else{
 log.info("交换机还未收到 id 为:{}消息,由于原因:{}",id,cause);
 }
 }
 //当消息无法路由的时候的回调方法
 @Override
 public void returnedMessage(Message message, int replyCode, String replyText, String 
exchange, String routingKey) {
 log.error(" 消 息 {}, 被交换机 {} 退回,退回原因 :{}, 路 由 key:{}",new 
String(message.getBody()),exchange,replyText,routingKey);
 }
}

 2.消息持久化

RabbitMQ确保消息不会丢失需要做两件事:我们需要将队列和消息都标记为持久化

2.1队列持久化

队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化

2.2消息持久化

让消息实现持久化需要在消息生产者修改代, MessageProperties.PERSISTENT_TEXT_PLAIN
加这个属性。

 3.消费手动确认

与生产消息类似,在消费消息时需要关闭自动确认,手动确认消息。

单次消息确认

// 假设已存在channel实例

boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // 确认一条消息成功传递,消息将会被RabbitMQ丢弃
             channel.basicAck(deliveryTag, false);
         }
     });

批量消息确认 

消费一条消息确认一次,效率太慢。可以进行批次确认。通过将 确认方法的的multiple参数设置为true。当multiple参数被设为true。RabbitMQ将会确认所有传递标签小于给定数值的消息。比如通道Ch上有未确认消息,它们的传递标签是5,6,7,8,如果有确认带的传递标签是8,且multiple参数被设为true,则5-8消息都会被确认;如果multiple参数被设为false,则5,6,7消息仍未被确认。示例如下:


boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             //确认所有传递标签小于deliveryTag的消息已成功传递,并丢弃它们
             channel.basicAck(deliveryTag, true);
         }
     });

确认消息失败重新入队

有时消费者处理能力较弱,但其他消费者有能力处理。这时候可以让消息重新入队让其它消费者进行处理。basic.reject和basic.nack两个方法通常被用于确认消息传递失败,MQ服务器可以丢弃这些消息或重新入队。可通过requeue参数来控制,当设为true时MQ服务器会将指定传递标签的消息重新入队,false会直接丢弃。


boolean autoAck = false;
channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // 确认传递标签为deliveryTag的消息传递失败,false:并丢弃它
             channel.basicReject(deliveryTag, false);
             // 确认传递标签为deliveryTag的消息传递失败,true:重新入队
             channel.basicReject(deliveryTag, true);
         }
     });

如果可能RabbitMQ会将重新入队的消息还放在它原来的位置,否则就放到尽可能离队首近的位置。假设某一瞬间出现,所有消费者的预取队列(prefetch)都已经满了(无法再接收消息),则会出现一个重新入队/重新传递的循环,造成网络带宽和内存资源的消耗。消费者需要追踪重新传递的数量,丢弃确认失败的消息,或经过一定时延后再重新入队。使用 basic.nack可以同时入队多条消息,它比basic.reject方法多了一个multiple参数。

    boolean autoAck = false;
    channel.basicConsume(queueName, autoAck, "a-consumer-tag",
     new DefaultConsumer(channel) {
         @Override
         public void handleDelivery(String consumerTag,
                                    Envelope envelope,
                                    AMQP.BasicProperties properties,
                                    byte[] body)
             throws IOException
         {
             long deliveryTag = envelope.getDeliveryTag();
             // 确认所有传递标签小于deliveryTag的消息传递失败,并重新入队它们
             channel.basicNack(deliveryTag, true, true);
             //单个deliveryTag的消息传递失败,并重新入队它
             channel.basicNack(deliveryTag, true, false);
         }
     });


 


 

有关RabbitMQ解决消息丢失的更多相关文章

  1. ruby-on-rails - 如何在 Rails View 上显示错误消息? - 2

    我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c

  2. ruby - 使用 Ruby 通过 Outlook 发送消息的最简单方法是什么? - 2

    我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=

  3. 屏幕录制为什么没声音?检查这2项,轻松解决 - 2

    相信很多人在录制视频的时候都会遇到各种各样的问题,比如录制的视频没有声音。屏幕录制为什么没声音?今天小编就和大家分享一下如何录制音画同步视频的具体操作方法。如果你有录制的视频没有声音,你可以试试这个方法。 一、检查是否打开电脑系统声音相信很多小伙伴在录制视频后会发现录制的视频没有声音,屏幕录制为什么没声音?如果当时没有打开音频录制,则录制好的视频是没有声音的。因此,建议在录制前进行检查。屏幕上没有声音,很可能是因为你的电脑系统的声音被禁止了。您只需打开电脑系统的声音,即可录制音频和图画同步视频。操作方法:步骤1:点击电脑屏幕右下侧的“小喇叭”图案,在上方的选项中,选择“声音”。 步骤2:在“声

  4. Ruby - 如何将消息长度表示为 2 个二进制字节 - 2

    我正在使用Ruby,我正在与一个网络端点通信,该端点在发送消息本身之前需要格式化“header”。header中的第一个字段必须是消息长度,它被定义为网络字节顺序中的2二进制字节消息长度。比如我的消息长度是1024。如何将1024表示为二进制双字节? 最佳答案 Ruby(以及Perl和Python等)中字节整理的标准工具是pack和unpack。ruby的packisinArray.您的长度应该是两个字节长,并且按网络字节顺序排列,这听起来像是n格式说明符的工作:n|Integer|16-bitunsigned,network(bi

  5. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  6. 深度学习部署:Windows安装pycocotools报错解决方法 - 2

    深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal

  7. ruby-on-rails - 在 Flash 警报 Rails 3 中显示错误消息 - 2

    如果我在模型中设置验证消息validates:name,:presence=>{:message=>'Thenamecantbeblank.'}我如何让该消息显示在闪光警报中,这是我迄今为止尝试过的方法defcreate@message=Message.new(params[:message])if@message.valid?ContactMailer.send_mail(@message).deliverredirect_to(root_path,:notice=>"Thanksforyourmessage,Iwillbeintouchsoon")elseflash[:error]

  8. ruby-on-rails - 在 RSpec 中,如何以任意顺序期望具有不同参数的多条消息? - 2

    RSpec似乎按顺序匹配方法接收的消息。我不确定如何使以下代码工作:allow(a).toreceive(:f)expect(a).toreceive(:f).with(2)a.f(1)a.f(2)a.f(3)我问的原因是a.f的一些调用是由我的代码的上层控制的,所以我不能对这些方法调用添加期望。 最佳答案 RSpecspy是测试这种情况的一种方式。要监视一个方法,用allowstub,除了方法名称之外没有任何约束,调用该方法,然后expect确切的方法调用。例如:allow(a).toreceive(:f)a.f(2)a.f(1)

  9. ruby-on-rails - 闪存消息存储在哪里? - 2

    我以为它们存储在cookie中-但不,检查cookie没有任何结果。session也不存储它们。那么,我在哪里可以找到它们?我需要这个来直接设置它们(而不是通过flashhash)。 最佳答案 它们存储在inyoursessionstore.自rails2.0以来的默认设置是cookie存储,但请检查config/initializers/session_store.rb以检查您是否使用默认设置以外的东西。 关于ruby-on-rails-闪存消息存储在哪里?,我们在StackOverf

  10. ruby - 如何更快地解决 project euler #21? - 2

    原始问题Letd(n)bedefinedasthesumofproperdivisorsofn(numberslessthannwhichdivideevenlyinton).Ifd(a)=bandd(b)=a,whereab,thenaandbareanamicablepairandeachofaandbarecalledamicablenumbers.Forexample,theproperdivisorsof220are1,2,4,5,10,11,20,22,44,55and110;therefored(220)=284.Theproperdivisorsof284are1,2,

随机推荐