面试题答案
一键面试可能出现一致性问题的场景
- 不同实例启动时间差异:部分实例启动较早,已经开始处理请求并计数,而新启动的实例计数器初始值为0,在一段时间内与老实例的计数状态不一致,可能导致新实例在短时间内处理过多请求,突破整体限流阈值。
- 网络延迟或分区:当网络出现延迟或发生网络分区时,不同实例间无法及时同步限流计数器信息。例如,一个实例的计数器达到限流阈值,但由于网络问题,其他实例没有收到这个信息,仍然继续处理请求,导致整体限流策略失效。
- 实例故障恢复:某个实例发生故障后恢复,其限流计数器可能重置为初始值,而其他正常运行的实例计数器处于正常计数状态,此时恢复的实例可能在短时间内处理过多请求,破坏限流一致性。
解决方法
- 集中式限流:
- 使用Redis:利用Redis的原子操作实现限流计数器。例如,使用
INCR
命令对计数器进行原子性递增。当一个请求到达微服务实例时,首先通过Redis的INCR
命令增加计数器的值,并检查是否超过限流阈值。由于Redis的单线程特性和原子操作保证,不同实例对计数器的操作是一致的。
@Autowired private StringRedisTemplate stringRedisTemplate; public boolean isAllowed(String key, int limit, long period) { String script = "local current\n" + "current = redis.call('INCR', KEYS[1])\n" + "if tonumber(current) == 1 then\n" + " redis.call('EXPIRE', KEYS[1], ARGV[1])\n" + "end\n" + "if tonumber(current) > tonumber(ARGV[2]) then\n" + " return 0\n" + "else\n" + " return 1\n" + "end"; List<String> keys = Collections.singletonList(key); List<String> args = Arrays.asList(String.valueOf(period), String.valueOf(limit)); return (Long) stringRedisTemplate.execute(new DefaultRedisScript<>(script, Long.class), keys, args) == 1; }
- 使用Zookeeper:通过Zookeeper的顺序节点特性来实现限流。每个实例在Zookeeper上创建一个顺序节点,根据节点序号和限流阈值判断是否允许处理请求。例如,限流阈值为100,当实例创建的顺序节点序号小于等于100时允许处理请求,大于100则限流。由于Zookeeper的强一致性保证,各个实例获取的节点序号是一致的,从而保证限流一致性。
- 使用Redis:利用Redis的原子操作实现限流计数器。例如,使用
- 分布式协调框架:
- 使用Eureka + 自定义同步机制:结合Eureka服务发现,在每个微服务实例中维护一个本地限流计数器,同时通过自定义的同步机制定期将本地计数器状态同步到Eureka。例如,每个实例定时向Eureka发送自己的计数器值,其他实例从Eureka获取最新的计数器汇总信息,从而保证各个实例的限流状态一致。
- 使用Consul:Consul提供了KV存储功能,可以将限流计数器存储在Consul的KV中。各个实例通过操作Consul的KV来同步限流计数器。例如,实例在处理请求时,先从Consul获取当前计数器值,更新后再写回Consul,利用Consul的一致性协议保证不同实例间计数器的一致性。
- 分布式消息队列:
- 使用Kafka:当一个实例的限流计数器发生变化时,将计数器变化信息发送到Kafka主题。其他实例从Kafka主题消费这些信息,更新本地的限流计数器。通过Kafka的消息有序性和持久化特性,保证各个实例能够按照相同的顺序更新计数器,从而达到限流一致性。