Há uma ou duas semanas, vi uma
mensagem no fórum
RabbitMQ Users sobre como configurar o envio de mensagens do SQL Server para o RabbitMQ. Como trabalhamos em estreita colaboração com a
Derivco , deixei algumas sugestões lá e também disse que estou escrevendo um blog sobre como fazer isso. Parte da minha mensagem não era inteiramente verdadeira - pelo menos até aquele momento (desculpe, irmão, ele estava muito ocupado).
Coisa impressionante, este é o seu
SQL Server . É muito fácil colocar informações em um banco de dados. Recuperar dados de um banco de dados usando uma consulta é igualmente fácil. Mas obter os dados atualizados ou colados já é um pouco mais difícil. Pense em eventos em tempo real; uma compra é feita - alguém precisa ser notificado sobre isso no momento exato em que isso aconteceu. Talvez alguém diga que esses dados não devem ser retirados do banco de dados, mas de algum outro lugar. Obviamente, esse é o caso, mas muitas vezes simplesmente não temos escolha.
Tivemos uma tarefa: enviar eventos do banco de dados para fora para processamento adicional, e a pergunta era - como fazer isso?
SQL Server e comunicações externas
Durante a existência do SQL Server, houve várias tentativas de organizar as comunicações fora do banco de dados;
Serviços de Notificação do SQL Server (NS), que apareceram no SQL Server 2000 e, posteriormente, no SQL Server 2005, o
SQL Server Service Broker (SSB) apareceu. Eu os descrevi em meu livro
A First Look at SQL Server 2005 for Developers , juntamente com Bob Boshemen e Dan Sullivan. O NS apareceu no SQL Server 2000, como eu disse, e foi redesenhado na versão beta do SQL Server 2005. No entanto, o NS foi
completamente excluído da versão pronta para venda (RTM) do SQL Server 2005.
Nota: Se você ler o livro, encontrará vários recursos que não estavam na versão RTM.
O SSB sobreviveu e a Microsoft lançou o
Service Broker External Activator (EA) em seu SQL Server 2008 Feature Pack. Permite através do SSB interagir fora do banco de dados local. Teoricamente, parece bom, mas na prática - é complicado e confuso. Fizemos alguns testes e rapidamente percebemos que não estava fazendo o que precisávamos. Além disso, o SSB não nos deu o desempenho necessário, por isso tivemos que inventar outra coisa.
SQLCLR
O que descobrimos como resultado foi baseado na tecnologia SQLCLR. SQLCLR é uma plataforma .NET integrada ao núcleo do SQL Server e pode ser usada para executar o código .NET dentro do kernel. Como executamos o código .NET, somos capazes de fazer quase tudo como em um aplicativo .NET comum.
Nota: eu escrevi "quase" acima, porque na verdade existem algumas limitações. Nesse contexto, essas restrições quase não têm efeito sobre o que vamos fazer.
O princípio de operação do SQLCLR é o seguinte: o código é compilado em uma biblioteca DLL e, em seguida, essa biblioteca é registrada usando as ferramentas do SQL Server:
Construir Montagem
CREATE ASSEMBLY [RabbitMQ.SqlServer] AUTHORIZATION rmq FROM 'F:\some_path\RabbitMQSqlClr4.dll' WITH PERMISSION_SET = UNSAFE; GO
Fragmento de código 1: Criando uma montagem ao longo de um caminho absoluto
O código executa as seguintes ações:
CREATE ASSEMBLY
- Cria uma montagem com o nome fornecido (não importa qual deve ser).AUTHORIZATION
- Indica o proprietário da montagem. Nesse caso, rmq é uma função predefinida do SQL Server.FROM
- Determina onde a montagem original está localizada. Na FROM
, você também pode especificar o caminho nos formatos binário ou UNC. Os arquivos de instalação para este projeto usam uma representação binária.WITH PERMISSION_SET
- Define permissões. UNSAFE
é o menos rigoroso e é necessário neste caso.
Nota: independentemente da função ou login usado na cláusula AUTHORIZATION
, a classe appdomain deve ser criada com o mesmo nome que ao carregar o assembly no domínio. É recomendável separar assemblies com nomes diferentes de classes de domínio de aplicativo para que, quando um assembly falhar, o restante não caia. No entanto, se as montagens dependem uma da outra, elas não podem ser divididas em classes diferentes.
Quando o assembly é criado, criamos wrappers de métodos .NET nele:
CREATE PROCEDURE rmq.pr_clr_PostRabbitMsg @EndpointID int, @Message nvarchar(max) AS EXTERNAL NAME [RabbitMQ.SqlServer].[RabbitMQSqlClr.RabbitMQSqlServer].[pr_clr_PostRabbitMsg]; GO
Snippet de código 2: Wrapper do método .NET
O código executa as seguintes ações:
- Cria um procedimento armazenado T-SQL chamado
rmq.pr_clr_PostRabbitMsg
que usa dois parâmetros; @EndpointID
e @Message
. - Em vez do corpo do procedimento, é usada uma fonte externa, que consiste em:
- Um assembly chamado
RabbitMQ.SqlServer
, ou seja, o agregado que criamos acima no snippet de código 1 . - Tipo completo (namespace e classe):
RabbitMQSqlClr.RabbitMQSqlServer
- O método do namespace e da classe acima é:
pr_clr_PostRabbitMsg
.
Quando
rmq.pr_clr_PostRabbitMsg
, o método
pr_clr_PostRabbitMsg
será chamado.
Nota: ao criar um procedimento, o nome da montagem não diferencia maiúsculas de minúsculas, ao contrário do nome completo do tipo e método. Não é necessário que o nome do procedimento que está sendo criado corresponda ao nome do método. No entanto, os tipos de dados finais para os parâmetros devem corresponder.
Como eu disse anteriormente, nós da Derivco precisamos enviar dados para fora do SQL Server, então usamos SQLCLR e
RabbitMQ (RMQ).
Rabbitmq
O RMQ é um intermediário de mensagens de código aberto que implementa o AMQP (Advanced Message Queuing Protocol) e é gravado em Erlang.
Como o RMQ é um intermediário de mensagens, as bibliotecas do cliente AMQP são necessárias para conectar-se a ele. O aplicativo se refere às bibliotecas clientes e, com a ajuda deles, abre uma conexão e envia mensagens - como, por exemplo, há uma chamada através do ADO.NET para o SQL Server. Mas, diferentemente do ADO.NET, onde, provavelmente, a conexão é aberta toda vez que você acessa o banco de dados, aqui a conexão permanece aberta por todo o período do aplicativo.
Portanto, para poder interagir do banco de dados com o RabbitMQ, precisamos do aplicativo e da biblioteca do cliente .NET para o RabbitMQ.
Nota: na próxima parte deste artigo, fragmentos de código RabbitMQ serão encontrados, mas sem explicações detalhadas do que eles fazem. Se você é novo no trabalho com o RabbitMQ, sugiro dar uma olhada nos vários tutoriais do RabbitMQ para entender o objetivo do código. O tutorial Hello World C # é um bom começo. Uma das diferenças entre livros didáticos e exemplos de código é que os trocadores não são declarados nos exemplos. Eles devem ser predefinidos.
RabbitMQ.SqlServer
RabbitMQ.SqlServer é um assembly que usa a biblioteca cliente .NET para RabbitMQ e fornece a capacidade de enviar mensagens do banco de dados para um ou mais pontos de extremidade RabbitMQ (VHosts e trocadores). O código pode ser baixado / bifurcado no meu repositório
RabbitMQ-SqlServer no GitHub. Ele contém fontes de montagem e arquivos de instalação (ou seja, você não precisa compilá-los).
Nota: este é apenas um exemplo para mostrar como o SQL Server pode interagir com o RabbitMQ. Este não é um produto acabado ou mesmo parte dele. Se esse código quebrar seu cérebro - não me culpe, porque este é apenas um exemplo.
Funcionalidade
Quando o assembly é carregado, ou quando sua inicialização é explicitamente chamada, ou quando é chamada indiretamente, no momento em que o procedimento do wrapper é chamado, o assembly carrega a cadeia de conexão no banco de dados local em que foi instalado, bem como nos terminais do RabbitMQ aos quais se conectam:
Ligação
internal bool InternalConnect() { try { connFactory = new ConnectionFactory(); connFactory.Uri = connString; connFactory.AutomaticRecoveryEnabled = true; connFactory.TopologyRecoveryEnabled = true; RabbitConn = connFactory.CreateConnection(); for (int x = 0; x < channels; x++) { var ch = RabbitConn.CreateModel(); rabbitChannels.Push(ch); } return true; } catch(Exception ex) { return false; } }
Fragmento de código 3: conectar-se ao terminal
Ao mesmo tempo, parte da conexão com o terminal também cria IModels na conexão e eles são usados ao enviar (adicionando à fila) mensagens:
Envio de mensagem
internal bool Post(string exchange, byte[] msg, string topic) { IModel value = null; int channelTryCount = 0; try { while ((!rabbitChannels.TryPop(out value)) && channelTryCount < 100) { channelTryCount += 1; Thread.Sleep(50); } if (channelTryCount == 100) { var errMsg = $"Channel pool blocked when trying to post message to Exchange: {exchange}."; throw new ApplicationException(errMsg); } value.BasicPublish(exchange, topic, false, null, msg); rabbitChannels.Push(value); return true; } catch (Exception ex) { if (value != null) { _rabbitChannels.Push(value); } throw; } }
O método
Post
é chamado a partir do método
pr_clr_PostRabbitMsg(int endPointId, string msgToPost)
, que foi apresentado como um procedimento usando a cláusula
CREATE PROCEDURE
no fragmento de código 2:
Método de pós-chamada
public static void pr_clr_PostRabbitMsg(int endPointId, string msgToPost) { try { if(endPointId == 0) { throw new ApplicationException("EndpointId cannot be 0"); } if (!isInitialised) { pr_clr_InitialiseRabbitMq(); } var msg = Encoding.UTF8.GetBytes(msgToPost); if (endPointId == -1) { foreach (var rep in remoteEndpoints) { var exch = rep.Value.Exchange; var topic = rep.Value.RoutingKey; foreach (var pub in rabbitPublishers.Values) { pub.Post(exch, msg, topic); } } } else { RabbitPublisher pub; if (rabbitPublishers.TryGetValue(endPointId, out pub)) { pub.Post(remoteEndpoints[endPointId].Exchange, msg, remoteEndpoints[endPointId].RoutingKey); } else { throw new ApplicationException($"EndpointId: {endPointId}, does not exist"); } } } catch { throw; } }
Fragmento de código 5: Representando um método como um procedimento
Quando o método é executado, supõe-se que o chamador envie o identificador do terminal para o qual a mensagem deve ser transmitida e, de fato, a própria mensagem. Se o valor -1 for passado como o identificador do nó de extremidade, iteramos sobre todos os pontos e enviamos uma mensagem para cada um deles. A mensagem vem na forma de uma sequência a partir da qual obtemos bytes usando
Encoding.UTF8.GetBytes
. Em um ambiente de produção, a chamada
Encoding.UTF8.GetBytes
deve ser substituída pela serialização.
Instalação
Para instalar e executar o exemplo, você precisa de todos os arquivos na pasta
src\SQL
. Para instalar, siga estes passos:
- Execute o script
01.create_database_and_role.sql
. Ele criará:
- Banco de dados de teste
RabbitMQTest
que o assembly será criado. rmq
a ser designada como proprietário da montagem- esquema, que também será chamado
rmq
. Neste diagrama, vários objetos de banco de dados são criados.
- Execute o arquivo
02.create_database_objects.sql
. Ele criará:
- a tabela
rmq.tb_RabbitSetting
, que armazenará a cadeia de conexão no banco de dados local. - A tabela
rmq.tb_RabbitEndpoint
, na qual um ou mais pontos de extremidade do RabbitMQ
serão armazenados.
- No arquivo
03.create_localhost_connstring.sql
altere o valor da variável @connString
para a cadeia de conexão correta do banco de dados RabbitMQTest
criado na etapa 1 e execute o script.
Antes de continuar, você deve ter uma instância em execução do broker RabbitMQ e do VHost (por padrão, o VHost é representado como /). Como regra, temos vários VHost, apenas para isolamento. Esse host também precisa de um trocador, no exemplo usamos
amq.topic
. Quando seu broker RabbitMQ estiver pronto, edite os
rmq.pr_UpsertRabbitEndpoint
procedimento
rmq.pr_UpsertRabbitEndpoint
, que estão localizados no arquivo
04.upsert_rabbit_endpoint.sql
:
Endpoint RabbitMQ
EXEC rmq.pr_UpsertRabbitEndpoint @Alias = 'rabbitEp1', @ServerName = 'RabbitServer', @Port = 5672, @VHost = 'testHost', @LoginName = 'rabbitAdmin', @LoginPassword = 'some_secret_password', @Exchange = 'amq.topic', @RoutingKey = '#', @ConnectionChannels = 5, @IsEnabled = 1
Fragmento de código 6: Criando um nó de extremidade no RabbitMQ
Neste ponto, é hora de implantar assemblies. Existem diferenças nas opções de implantação para versões do SQL Server anteriores ao SQL Server 2014 (2005, 2008, 2008R2, 2012) e para 2014 e posteriores. A diferença está na versão suportada do CLR. Antes do SQL Server 2014, a plataforma .NET estava em execução no CLR versão 2 e, no SQL Server 2014 e superior, a versão 4 era usada.
SQL Server 2005 - 2012
Vamos começar com as versões do SQL Server executadas no CLR 2, pois elas têm suas próprias características. Precisamos implantar o assembly criado e, ao mesmo tempo, implantar a biblioteca .NET do cliente
RabbitMQ.Client
(
RabbitMQ.Client
). Da nossa montagem, nos referiremos à biblioteca do cliente RabbitMQ. Porque Como planejamos usar o CLR 2, nosso assembly e o
RabbitMQ.Client
devem ser compilados com base no .NET 3.5. Há problemas
Todas as versões mais recentes da biblioteca
RabbitMQ.Client
são compiladas para o ambiente CLR 4, portanto, não podem ser usadas em nosso assembly. A versão mais recente das bibliotecas cliente para CLR 2 é compilada no .NET 3.4.3. Mas, mesmo se tentarmos implantar esse assembly, receberemos uma mensagem de erro:
Figura 1: Conjunto System.ServiceModel ausenteEsta versão do
RabbitMQ.Client
refere-se a um assembly que não faz parte do SQL Server CLR. Este é um assembly do WCF e esta é uma das limitações do SQLCLR que eu mencionei acima: esse assembly específico é para tipos de tarefas que não podem ser executadas no SQL Server. As versões recentes do
RabbitMQ.Client
não possuem essas dependências, portanto podem ser usadas sem problemas, exceto pelos requisitos irritantes do CLR 4. O que devo fazer?
Como você sabe, o RabbitMQ é de código aberto, mas somos desenvolvedores, certo? ;) Então vamos recompilar! Na versão anterior às versões mais recentes (ou seja, versão <3.5.0) do
RabbitMQ.Client
excluí os links para
System.ServiceModel
e recompilei. Eu tive que alterar algumas linhas de código usando a funcionalidade
System.ServiceModel
, mas essas foram pequenas alterações.
Neste exemplo, eu não usei a versão 3.4.3 do cliente, mas peguei a
versão estável 3.6.6 e recompilei usando o .NET 3.5 (CLR 2). Quase funcionou :), exceto que as versões posteriores do
RabbitMQ.Client
usam
Task
'e que não fazem parte originalmente do .NET 3.5.
Felizmente, existe uma versão do
System.Threading.dll
para .NET 3.5 que inclui a
Task
. Eu baixei, configurei os links e tudo correu! Aqui, o truque principal é que o
System.Threading.dll
deve ser instalado com o assembly.
Nota: a fonte do RabbitMQ.Client
, da qual compilei uma versão do .NET 3.5, está no meu repositório no GitHub RabbitMQ Client 3.6.6 .NET 3.5 . A DLL de binário, juntamente com System.Threading.dll
para .NET 3.5, também está no lib\NET3.5
repositório (RabbitMQ-SqlServer) .
Para instalar os assemblies necessários (
System.Threading
,
RabbitMQ.Client
e
RabbitMQ.SqlServer
), execute os scripts de instalação no diretório
src\sql
na seguinte ordem:
05.51.System.Threading.sql2k5-12.sql
- System.Threading05.52.RabbitMQ.Client.sql2k5-12.sql
- RabbitMQ.Client05.53.RabbitMQ.SqlServer.sql2k5-12.sql
- RabbitMQ.SqlServer
SQL Server 2014 ou superior
No SQL Server 2014 e posterior, o assembly é compilado no .NET 4.XX (meu exemplo está no 4.5.2) e você pode fazer referência a qualquer uma das versões mais recentes do
RabbitMQ.Client
, que podem ser obtidas usando o
NuGet . No meu exemplo, estou usando o 4.1.1.
RabbitMQ.Client
, que também está no
lib\NET4
repositório (RabbitMQ-SqlServer) .
Para instalar, execute os scripts no diretório
src\sql
na seguinte ordem:
05.141.RabbitMQ.Client.sql2k14+.sql
- RabbitMQ.Client05.142.RabbitMQ.SqlServer.sql2k14+.sql
- RabbitMQ.SqlServer
Wrappers de método SQL
Para criar procedimentos que serão usados em nossa montagem (3.5 ou 4), execute o script
06.create_sqlclr_procedures.sql
. Ele criará procedimentos T-SQL para três métodos .NET:
rmq.pr_clr_InitialiseRabbitMq
chama pr_clr_InitialiseRabbitMq
. Usado para carregar e inicializar o assembly RabbitMQ.SqlServer.rmq.pr_clr_ReloadRabbitEndpoints
chama pr_clr_ReloadRabbitEndpoints
. Carrega vários pontos de extremidade do RabbitMQ.rmq.pr_clr_PostRabbitMsg
chama pr_clr_PostRabbitMsg
. Usado para enviar mensagens para RabbitMQ.
O script também cria um procedimento T-SQL simples -
rmq.pr_PostRabbitMsg
, que se aplica a
rmq.pr_clr_PostRabbitMsg
. Este é um procedimento de invólucro que sabe o que fazer com dados, manipula exceções etc. Em um ambiente de produção, temos vários procedimentos semelhantes que processam vários tipos de mensagens. Leia mais sobre isso abaixo.
Use
De todas as
rmq.pr_PostRabbitMsg
acima, é claro que, para enviar mensagens ao RabbitMQ, chamamos
rmq.pr_PostRabbitMsg
ou
rmq.pr_clr_PostRabbitMsg
, transmitindo nos parâmetros o identificador do terminal e a própria mensagem como uma string. Tudo isso, é claro, é legal, mas eu gostaria de ver como isso funcionará na realidade.
O que fazemos nos ambientes de produção é que, nos procedimentos armazenados que processam os dados que devem ser enviados ao RabbitMQ, coletamos os dados a serem enviados e, no bloco de conexão, chamamos um procedimento como
rmq.pr_PostRabbitMsg
. A seguir, é apresentado um exemplo muito simplificado de tal procedimento:
Procedimento de processamento de dados
ALTER PROCEDURE dbo.pr_SomeProcessingStuff @id int AS BEGIN SET NOCOUNT ON; BEGIN TRY
No
fragmento de código 7, vemos como os dados necessários são capturados e processados no procedimento e enviados após o processamento. Para usar este procedimento, execute o script
07.create_processing_procedure.sql
no diretório
src\SQL
.
Vamos rodar tudo
Neste ponto, você deve estar preparado para enviar algumas mensagens. Antes do teste, verifique se você possui filas no RabbitMQ conectadas ao trocador de terminal em
rmq.tb_RabbitEndpoint
.
Portanto, para começar, você precisa fazer o seguinte:
Abra o arquivo
99.test_send_message.sql
.
Executar
EXEC rmq.pr_clr_InitialiseRabbitMq;
para inicializar a montagem e carregar os pontos de extremidade do RabbitMQ. Esta não é uma etapa necessária, mas é recomendável pré-carregar a montagem após criar ou modificar.
Executar
EXEC dbo.pr_SomeProcessingStuff @id = 101
(você pode usar qualquer outro identificador que desejar).
Se tudo funcionou sem erros, uma mensagem deve aparecer na fila do RabbitMQ! Então você usou o SQLCLR para enviar uma mensagem ao RabbitMQ.
Parabéns!