使用Spring Boot + Redis 進行實時流處理 - vinsguru

banq發表於2020-10-21

Redis用於快取記憶體資料。除了主/從複製,釋出/訂閱功能和時間序列資料支援外,還新增了對流的支援。Kafka的問題是很難配置。基礎架構維護非常具有挑戰性。但是Redis非常容易並且重量輕。
 
樣例應用:

使用Spring Boot +  Redis 進行實時流處理 - vinsguru

  • 我們的釋出者將釋出與Redis購買相關的一些偶數。讓我們稱它們為購買事件流。
  • 消費者群體有興趣聽這些事件。這可能是用於計算收入或處理付款或傳送電子郵件!
    • 當您需要執行所有這些操作時,例如:付款處理和傳送電子郵件,則每個使用者都需要一個單獨的消費者組。
  • 消費者將消費事件,他們可以做任何事情。在我們的案例中,我們只是找到使用者支付的價格,然後按類別計算收入。
    • 可以將其記錄在單獨的資料庫中。但是為了簡單起見,我將在Redis中將此資訊記錄為SortedSet。

原始碼:
完整的原始碼在這裡
 

生產者
理想情況下,生產者和消費者將是兩個不同的微服務/應用程式。在這裡,我們兩個人都將在同一個專案中。但是,我們基於名為“ app.role ”的自定義屬性來控制應用程式的行為,使其像生產者還是消費者。基於該值,將在Spring中建立相應的元件。

@Service
@ConditionalOnProperty(name="app.role", havingValue="producer")
public class PurchaseEventProducer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    @Value("${stream.key}")
    private String streamKey;

    @Autowired
    private ProductRepository repository;

    @Autowired
    private ReactiveRedisTemplate<String, String> redisTemplate;

    @Scheduled(fixedRateString= "${publish.rate}")
    public void publishEvent(){
        Product product = this.repository.getRandomProduct();
        ObjectRecord<String, Product> record = StreamRecords.newRecord()
                .ofObject(product)
                .withStreamKey(streamKey);
        this.redisTemplate
                .opsForStream()
                .add(record)
                .subscribe(System.out::println);
        atomicInteger.incrementAndGet();
    }

    @Scheduled(fixedRate = 10000)
    public void showPublishedEventsSoFar(){
        System.out.println(
                "Total Events :: " + atomicInteger.get()
        );
    }

}

  • publishEvent方法定期釋出一些隨機購買的產品。
  • showPublishedEventsSoFar方法僅顯示到目前為止已下的訂單數。

 

消費者
我們的釋出者已經準備好。讓我們建立一個消費者。要使用RedisStreams,我們需要實現StreamListener介面。

@Service
@ConditionalOnProperty(name="app.role", havingValue="consumer")
public class PurchaseEventConsumer implements StreamListener<String, ObjectRecord<String, Product>> {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    @Autowired
    private ReactiveRedisTemplate<String, String> redisTemplate;

    @Override
    @SneakyThrows
    public void onMessage(ObjectRecord<String, Product> record) {
        System.out.println(
                InetAddress.getLocalHost().getHostName() + " - consumed :" +
                record.getValue()
        );
        this.redisTemplate
                .opsForZSet()
                .incrementScore("revenue", record.getValue().getCategory().toString(), record.getValue().getPrice())
                .subscribe();
        atomicInteger.incrementAndGet();
    }

    @Scheduled(fixedRate = 10000)
    public void showPublishedEventsSoFar(){
        System.out.println(
                "Total Consumed :: " + atomicInteger.get()
        );
    }

}

  • 我們只簡單地顯示消費記錄。
  • 然後,我們獲得支付的價格並將其新增到redis排序集中。
  • 像釋出者一樣,我們會定期顯示此使用者消耗的事件數。

 

Redis流配置
建立使用者後,我們需要透過將上述使用者新增到StreamMessageListenerContainer例項中來建立訂閱。

@Configuration
@ConditionalOnProperty(name="app.role", havingValue="consumer")
public class RedisStreamConfig {

    @Value("${stream.key}")
    private String streamKey;

    @Autowired
    private StreamListener<String, ObjectRecord<String, Product>> streamListener;

    @Bean
    public Subscription subscription(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        var options = StreamMessageListenerContainer
                            .StreamMessageListenerContainerOptions
                            .builder()
                            .pollTimeout(Duration.ofSeconds(1))
                            .targetType(Product.class)
                            .build();
        var listenerContainer = StreamMessageListenerContainer
                                    .create(redisConnectionFactory, options);
        var subscription = listenerContainer.receiveAutoAck(
                Consumer.from(streamKey, InetAddress.getLocalHost().getHostName()),
                StreamOffset.create(streamKey, ReadOffset.lastConsumed()),
                streamListener);
        listenerContainer.start();
        return subscription;
    }

}



詳細執行配置見原文
 

相關文章