最近一個外部的專案,使用到了訊息佇列,本來是用rabbitmq實現的,但是由於是部署到別人家的伺服器上,想盡量簡化一些,專案中本來也要接入了redis快取,就嘗試使用redis來實現簡單的訊息佇列。
使用redis做訊息佇列有兩種方法,一種是使用pub/sub,另一種是使用list結構,配合brpop來消費。這兩種方式各有特點,這裡簡述一下:
- pub/sub模式,支援多客戶端消費,但是不支援持久化,這就意味著客戶端斷開的時間內釋出的訊息將會全部捨棄掉。
- list配合brpop,預設不支援多客戶端消費,支援持久化。這種模式的多客戶端消費可以變相實現,比如下面的虛擬碼:
#第一步push訊息到佇列
lpush listA msg
#第二步,一個專門的分發客戶端取出訊息,push到各個子佇列
var msg=brpop listA
lpush listA1 msg
lpush listA2 msg
......
#第三步,多個客戶端從對應的佇列消費訊息
var client1_msg= brpop listA1
var client2_msg= brpop listA2
......
訊息丟失不太可取,所以我選擇了list ,下一步需要選擇一個合適的客戶端。
Stackexchange.redis 算是一個老牌的客戶端了,但是由於其採用多路複用的模式,沒法支援Redis的blocking pops特性。所以我採用了國人寫的CSRedisCore。
首先需要在appsettings.json中新增redis的連線字串:
{
"ConnectionStrings": {
"redis": "{ip}:{port},password=123456,prefix=my_"
}
}
具體配置請參考github上的文件:https://github.com/2881099/csredis
然後在startup.cs的ConfigureServices中配置redis:
public void ConfigureServices(IServiceCollection services)
{
//redis配置
RedisHelper.Initialization(new CSRedis.CSRedisClient(Configuration.GetConnectionString("redis")));
}
當然也可以採用依賴注入的方式新增CSRedisClient例項,這個不糾結。
在專案中有好幾處使用到了佇列,所以先封裝一個消費服務:
public abstract class RedisMQConsumer : BackgroundService
{
protected abstract string CacheKey { get; }
protected ILogger<RedisMQConsumer> logger;
public RedisMQConsumer(ILogger<RedisMQConsumer> logger)
{
this.logger = logger;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.Run( async() =>
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var msg = RedisHelper.BRPop(5, CacheKey);
try
{
if (string.IsNullOrEmpty(msg)) continue;
if (!Process(msg))
{
//加入錯誤處理佇列,可以在後臺寫功能手動處理
RedisHelper.LPush(CacheKey + "_err", msg);
}
}
catch (Exception exp)
{
//加入錯誤處理佇列,可以在後臺寫功能手動處理
RedisHelper.LPush(CacheKey + "_err", msg);
logger.LogError(exp, "RedisMQConsumer Execute error");
}
}
catch
{
//網路可能中斷
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
}
}
}, stoppingToken);
}
protected abstract bool Process(string message);
}
然後就可以繼承RedisMQConsumer,編寫實際邏輯:
public class AddOrderMQConsumer : RedisMQConsumer
{
public AddOrderMQConsumer(ILogger<RedisMQConsumer> logger) : base(logger)
{
}
protected override string CacheKey => "addOrder";
protected override bool Process(string message)
{
var order = JsonSerializer.Deserialize<Order>(message);
//處理邏輯
return true;
}
}
釋出訊息只是往佇列中新增項:
RedisHelper.LPush("addOrder", order);
最後把消費服務新增到startup.cs中:
public void ConfigureServices(IServiceCollection services)
{
//redis配置
RedisHelper.Initialization(new CSRedis.CSRedisClient(Configuration.GetConnectionString("redis")));
//redis訊息佇列消費服務,放在redis配置下方
services.AddHostedService<AddOrderMQConsumer>();
}
經測試,還算穩定,小併發專案可以使用。