Inicio rápido: Uso de Azure Cache for Redis con .NET Core

En este inicio rápido incorporará Azure Redis Cache en una aplicación .NET Core para acceder a una caché dedicada y segura, a la que se puede acceder desde cualquier aplicación de Azure. Concretamente, usará el cliente StackExchange.Redis con código C# en una aplicación de consola de .NET Core.

Ir al código en GitHub

Si quiere pasar directamente al código, consulte Inicio rápido: Uso de Azure Cache for Redis en .NET Core en GitHub.

Requisitos previos

Creación de una caché

  1. Para crear una instancia de caché, inicie sesión en Azure Portal y seleccione Crear un recurso.

    Crear un recurso está resaltado en el panel de navegación izquierdo.

  2. En la página Nuevo, seleccione Base de datos y, a continuación, seleccione Azure Cache for Redis.

    En Nuevo, están resaltados Bases de datos y Azure Cache for Redis.

  3. En la página Nueva instancia de Redis Cache, configure las opciones de la nueva caché.

    Configuración Elegir un valor Descripción
    Suscripción Desplácese hacia abajo y seleccione su suscripción. La suscripción en la que se creará esta nueva instancia de Azure Cache for Redis.
    Grupos de recursos Desplácese hacia abajo y seleccione un grupo de recursos o Crear nuevo y escriba un nombre nuevo para el grupo de recursos. Nombre del grupo de recursos en el que se van a crear la caché y otros recursos. Al colocar todos los recursos de la aplicación en un grupo de recursos, puede administrarlos o eliminarlos fácilmente.
    Nombre DNS Escriba un nombre único. El nombre de la memoria caché debe ser una cadena de entre 1 y 63 caracteres, y solo puede contener números, letras o guiones. El nombre debe comenzar y terminar por un número o una letra y no puede contener guiones consecutivos. El nombre de host de la instancia de caché será <DNS name>.redis.cache.windows.net.
    Ubicación Desplácese hacia abajo y seleccione una ubicación. Seleccione una región cerca de otros servicios que vayan a usar la memoria caché.
    Tipo de caché Desplácese hacia abajo y seleccione un nivel. El nivel determina el tamaño, rendimiento y características disponibles para la memoria caché. Para más información, consulte la introducción a Azure Redis Cache.
  4. Seleccione la pestaña Redes o elija el botón Redes situado en la parte inferior de la página.

  5. En la pestaña Redes, seleccione el método de conectividad.

  6. Seleccione la pestaña Siguiente: Opciones avanzadas o seleccione el botón Siguiente: Opciones avanzadas en la parte inferior de la página.

  7. En la pestaña Opciones avanzadas de una instancia de caché básica o estándar, seleccione el botón de alternancia de habilitación si desea habilitar un puerto que no sea TLS. También puede seleccionar la versión de Redis que quiere usar, 4 o 6.

    Versiones 4 o 6 de Redis.

  8. En la pestaña Opciones avanzadas de la instancia de caché Premium, configure el puerto no TLS, la agrupación en clústeres y la persistencia de datos. También puede seleccionar la versión de Redis que quiere usar, 4 o 6.

  9. Seleccione el botón Siguiente: Opciones avanzadas o elija el botón Siguiente: Etiquetas situado en la parte inferior de la página.

  10. Opcionalmente, en la pestaña Etiquetas, escriba el nombre y el valor si desea clasificar el recurso.

  11. Seleccione Revisar + crear. Pasará a la pestaña Revisar y crear, donde Azure validará la configuración.

  12. Tras aparecer el mensaje verde Validación superada, seleccione Crear.

La caché tarda un tiempo en crearse. Puede supervisar el progreso en la página Información general de Azure Cache for Redis. Cuando Estado se muestra como En ejecución, la memoria caché está lista para su uso.

Recuperación del nombre de host, los puertos y las claves de acceso desde Azure Portal

Para conectar con una instancia de Azure Cache for Redis, los clientes de dicha caché necesitan el nombre de host, los puertos y una clave para la caché. Es posible que algunos clientes utilicen nombres ligeramente diferentes para estos elementos. Puede obtener el nombre de host, los puertos y las claves de Azure Portal.

  • Para obtener las claves de acceso, en el panel de navegación izquierdo de Azure Cache for Redis, seleccione Claves de acceso.

    Claves de Azure Redis Cache

  • Para obtener el nombre de host y los puertos, en el panel de navegación izquierdo de Azure Cache for Redis, seleccione Propiedades. El nombre de host tiene el formato <DNS name>.redis.cache.windows.net.

    Propiedades de Azure Redis Cache

Tome nota del NOMBRE DE HOST y de la clave de acceso principal. Usará estos valores más adelante para construir el secreto CacheConnection.

Crear una aplicación de consola

Abra una nueva ventana Comandos y ejecute el siguiente comando para crear una nueva aplicación de consola .NET Core:

dotnet new console -o Redistest

En la ventana Comandos, cambie al nuevo directorio del proyecto Redistest.

Adición de Secret Manager al proyecto

En esta sección, agregará la herramienta Secret Manager al proyecto. La herramienta Secret Manager almacena información confidencial para el trabajo de desarrollo fuera de su árbol de proyecto. Este enfoque ayuda a evitar el uso compartido accidental de secretos de la aplicación en el código fuente.

Abra el archivo Redistest.csproj. Agregue un elemento DotNetCliToolReference para incluir Microsoft.Extensions.SecretManager.Tools. Además, agregue un elemento UserSecretsId como se muestra a continuación y guarde el archivo.

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <UserSecretsId>Redistest</UserSecretsId>
    </PropertyGroup>
    <ItemGroup>
        <DotNetCliToolReference Include="Microsoft.Extensions.SecretManager.Tools" Version="2.0.0" />
    </ItemGroup>
</Project>

Ejecute el siguiente comando para agregar el paquete Microsoft.Extensions.Configuration.UserSecrets al proyecto:

dotnet add package Microsoft.Extensions.Configuration.UserSecrets

Ejecute el siguiente comando para restaurar los paquetes:

dotnet restore

En la ventana Comandos, ejecute el siguiente comando para almacenar un secreto nuevo denominado CacheConnection después de reemplazar los marcadores de posición (incluidos los corchetes angulares) por el nombre de la caché y la clave de acceso principal:

dotnet user-secrets set CacheConnection "<cache name>.redis.cache.windows.net,abortConnect=false,ssl=true,allowAdmin=true,password=<primary-access-key>"

Agregue la siguiente instrucción using a Program.cs:

using Microsoft.Extensions.Configuration;

Agregue los siguientes miembros a la clase Program de Program.cs. Este código inicializa una configuración para acceder al secreto de usuario de la cadena de conexión de Azure Redis Cache.

private static IConfigurationRoot Configuration { get; set; }
const string SecretName = "CacheConnection";

private static void InitializeConfiguration()
{
    var builder = new ConfigurationBuilder()
        .AddUserSecrets<Program>();

    Configuration = builder.Build();
}

Configuración del cliente de caché

En esta sección, configurará la aplicación de consola para utilizar el cliente StackExchange.Redis para .NET.

En la ventana Comandos, ejecute el siguiente comando en el directorio del proyecto Redistest:

dotnet add package StackExchange.Redis

Una vez completada la instalación, el cliente de caché StackExchange.Redis está disponible para su uso con el proyecto.

Conexión a la memoria caché

Agregue la siguiente instrucción using a Program.cs:

using StackExchange.Redis;

La clase ConnectionMultiplexer administra la conexión con Azure Redis Cache. Esta clase debe compartirse y reutilizarse en toda la aplicación cliente. No cree una nueva conexión para cada operación.

En Program.cs, agregue los siguientes miembros a la clase Program de la aplicación de consola:

private static Lazy<ConnectionMultiplexer> lazyConnection = CreateConnection();

public static ConnectionMultiplexer Connection
{
    get
    {
        return lazyConnection.Value;
    }
}

private static Lazy<ConnectionMultiplexer> CreateConnection()
{
    return new Lazy<ConnectionMultiplexer>(() =>
    {
        string cacheConnection = Configuration[SecretName];
        return ConnectionMultiplexer.Connect(cacheConnection);
    });
}

Este enfoque para compartir una instancia de ConnectionMultiplexer en la aplicación usa una propiedad estática que devuelve una instancia conectada. El código proporciona una manera segura para subprocesos de inicializar solo una instancia de ConnectionMultiplexer conectada. abortConnect está establecido en false, lo que significa que la llamada se realizará correctamente incluso si no se establece ninguna conexión a Azure Redis Cache. Una de las características principales de ConnectionMultiplexer es que restaura automáticamente la conectividad a la caché una vez que el problema de red u otras causas se resuelven.

Se accede al valor del secreto CacheConnection mediante el proveedor de configuración del Administrador de secretos y se utiliza como parámetro de contraseña.

Control de RedisConnectionException y SocketException mediante reconexión

Un procedimiento recomendado a la hora de llamar a los métodos de ConnectionMultiplexer es intentar resolver las excepciones RedisConnectionException y SocketException automáticamente cerrando y reestableciendo la conexión.

Agregue las siguientes instrucciones using a Program.cs:

using System.Net.Sockets;
using System.Threading;

En Program.cs, agregue los siguientes miembros a la clase Program:

private static IConfigurationRoot Configuration { get; set; }
private static long _lastReconnectTicks = DateTimeOffset.MinValue.UtcTicks;
private static DateTimeOffset _firstErrorTime = DateTimeOffset.MinValue;
private static DateTimeOffset _previousErrorTime = DateTimeOffset.MinValue;
private static SemaphoreSlim _reconnectSemaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
private static SemaphoreSlim _initSemaphore = new SemaphoreSlim(initialCount: 1, maxCount: 1);
private static ConnectionMultiplexer _connection;
private static bool _didInitialize = false;
// In general, let StackExchange.Redis handle most reconnects,
// so limit the frequency of how often ForceReconnect() will
// actually reconnect.
public static TimeSpan ReconnectMinInterval => TimeSpan.FromSeconds(60);
// If errors continue for longer than the below threshold, then the
// multiplexer seems to not be reconnecting, so ForceReconnect() will
// re-create the multiplexer.
public static TimeSpan ReconnectErrorThreshold => TimeSpan.FromSeconds(30);
public static TimeSpan RestartConnectionTimeout => TimeSpan.FromSeconds(15);
public static int RetryMaxAttempts => 5;

public static ConnectionMultiplexer Connection { get { return _connection; } }
private static async Task InitializeAsync()
{
    if (_didInitialize)
    {
        throw new InvalidOperationException("Cannot initialize more than once.");
    }
    var builder = new ConfigurationBuilder()
        .AddUserSecrets<Program>();
    Configuration = builder.Build();
    _connection = await CreateConnectionAsync();
    _didInitialize = true;
}
// This method may return null if it fails to acquire the semaphore in time.
// Use the return value to update the "connection" field
private static async Task<ConnectionMultiplexer> CreateConnectionAsync()
{
    if (_connection != null)
    {
        // If we already have a good connection, let's re-use it
        return _connection;
    }
    try
    {
        await _initSemaphore.WaitAsync(RestartConnectionTimeout);
    }
    catch
    {
        // We failed to enter the semaphore in the given amount of time. Connection will either be null, or have a value that was created by another thread.
        return _connection;
    }
    // We entered the semaphore successfully.
    try
    {
        if (_connection != null)
        {
            // Another thread must have finished creating a new connection while we were waiting to enter the semaphore. Let's use it
            return _connection;
        }
        // Otherwise, we really need to create a new connection.
        string cacheConnection = Configuration["CacheConnection"].ToString();
        return await ConnectionMultiplexer.ConnectAsync(cacheConnection);
    }
    finally
    {
        _initSemaphore.Release();
    }
}
private static async Task CloseConnectionAsync(ConnectionMultiplexer oldConnection)
{
    if (oldConnection == null)
    {
        return;
    }
    try
    {
        await oldConnection.CloseAsync();
    }
    catch (Exception)
    {
        // Ignore any errors from the oldConnection
    }
}
/// <summary>
/// Force a new ConnectionMultiplexer to be created.
/// NOTES:
///     1. Users of the ConnectionMultiplexer MUST handle ObjectDisposedExceptions, which can now happen as a result of calling ForceReconnectAsync().
///     2. Call ForceReconnectAsync() for RedisConnectionExceptions and RedisSocketExceptions. You can also call it for RedisTimeoutExceptions,
///         but only if you're using generous ReconnectMinInterval and ReconnectErrorThreshold. Otherwise, establishing new connections can cause
///         a cascade failure on a server that's timing out because it's already overloaded.
///     3. The code will:
///         a. wait to reconnect for at least the "ReconnectErrorThreshold" time of repeated errors before actually reconnecting
///         b. not reconnect more frequently than configured in "ReconnectMinInterval"
/// </summary>
public static async Task ForceReconnectAsync()
{
    var utcNow = DateTimeOffset.UtcNow;
    long previousTicks = Interlocked.Read(ref _lastReconnectTicks);
    var previousReconnectTime = new DateTimeOffset(previousTicks, TimeSpan.Zero);
    TimeSpan elapsedSinceLastReconnect = utcNow - previousReconnectTime;
    // If multiple threads call ForceReconnectAsync at the same time, we only want to honor one of them.
    if (elapsedSinceLastReconnect < ReconnectMinInterval)
    {
        return;
    }
    try
    {
        await _reconnectSemaphore.WaitAsync(RestartConnectionTimeout);
    }
    catch
    {
        // If we fail to enter the semaphore, then it is possible that another thread has already done so.
        // ForceReconnectAsync() can be retried while connectivity problems persist.
        return;
    }
    try
    {
        utcNow = DateTimeOffset.UtcNow;
        elapsedSinceLastReconnect = utcNow - previousReconnectTime;
        if (_firstErrorTime == DateTimeOffset.MinValue)
        {
            // We haven't seen an error since last reconnect, so set initial values.
            _firstErrorTime = utcNow;
            _previousErrorTime = utcNow;
            return;
        }
        if (elapsedSinceLastReconnect < ReconnectMinInterval)
        {
            return; // Some other thread made it through the check and the lock, so nothing to do.
        }
        TimeSpan elapsedSinceFirstError = utcNow - _firstErrorTime;
        TimeSpan elapsedSinceMostRecentError = utcNow - _previousErrorTime;
        bool shouldReconnect =
            elapsedSinceFirstError >= ReconnectErrorThreshold // Make sure we gave the multiplexer enough time to reconnect on its own if it could.
            && elapsedSinceMostRecentError <= ReconnectErrorThreshold; // Make sure we aren't working on stale data (e.g. if there was a gap in errors, don't reconnect yet).
        // Update the previousErrorTime timestamp to be now (e.g. this reconnect request).
        _previousErrorTime = utcNow;
        if (!shouldReconnect)
        {
            return;
        }
        _firstErrorTime = DateTimeOffset.MinValue;
        _previousErrorTime = DateTimeOffset.MinValue;
        ConnectionMultiplexer oldConnection = _connection;
        await CloseConnectionAsync(oldConnection);
        _connection = null;
        _connection = await CreateConnectionAsync();
        Interlocked.Exchange(ref _lastReconnectTicks, utcNow.UtcTicks);
    }
    finally
    {
        _reconnectSemaphore.Release();
    }
}
// In real applications, consider using a framework such as
// Polly to make it easier to customize the retry approach.
private static async Task<T> BasicRetryAsync<T>(Func<T> func)
{
    int reconnectRetry = 0;
    int disposedRetry = 0;
    while (true)
    {
        try
        {
            return func();
        }
        catch (Exception ex) when (ex is RedisConnectionException || ex is SocketException)
        {
            reconnectRetry++;
            if (reconnectRetry > RetryMaxAttempts)
                throw;
            await ForceReconnectAsync();
        }
        catch (ObjectDisposedException)
        {
            disposedRetry++;
            if (disposedRetry > RetryMaxAttempts)
                throw;
        }
    }
}
public static Task<IDatabase> GetDatabaseAsync()
{
    return BasicRetryAsync(() => Connection.GetDatabase());
}
public static Task<System.Net.EndPoint[]> GetEndPointsAsync()
{
    return BasicRetryAsync(() => Connection.GetEndPoints());
}
public static Task<IServer> GetServerAsync(string host, int port)
{
    return BasicRetryAsync(() => Connection.GetServer(host, port));
}

Ejecución de comandos de caché

En Program.cs, agregue el código siguiente al procedimiento Main de la clase Program de la aplicación de consola:

static void Main(string[] args)
{
    InitializeConfiguration();

    IDatabase cache = GetDatabase();

    // Perform cache operations using the cache object...

    // Simple PING command
    string cacheCommand = "PING";
    Console.WriteLine("\nCache command  : " + cacheCommand);
    Console.WriteLine("Cache response : " + cache.Execute(cacheCommand).ToString());

    // Simple get and put of integral data types into the cache
    cacheCommand = "GET Message";
    Console.WriteLine("\nCache command  : " + cacheCommand + " or StringGet()");
    Console.WriteLine("Cache response : " + cache.StringGet("Message").ToString());

    cacheCommand = "SET Message \"Hello! The cache is working from a .NET Core console app!\"";
    Console.WriteLine("\nCache command  : " + cacheCommand + " or StringSet()");
    Console.WriteLine("Cache response : " + cache.StringSet("Message", "Hello! The cache is working from a .NET Core console app!").ToString());

    // Demonstrate "SET Message" executed as expected...
    cacheCommand = "GET Message";
    Console.WriteLine("\nCache command  : " + cacheCommand + " or StringGet()");
    Console.WriteLine("Cache response : " + cache.StringGet("Message").ToString());

    // Get the client list, useful to see if connection list is growing...
    // Note that this requires allowAdmin=true in the connection string
    cacheCommand = "CLIENT LIST";
    Console.WriteLine("\nCache command  : " + cacheCommand);
    var endpoint = (System.Net.DnsEndPoint)GetEndPoints()[0];
    IServer server = GetServer(endpoint.Host, endpoint.Port);
    ClientInfo[] clients = server.ClientList();

    Console.WriteLine("Cache response :");
    foreach (ClientInfo client in clients)
    {
        Console.WriteLine(client.Raw);
    }

    CloseConnection(lazyConnection);
}

Guarde Program.cs.

Las instancias de Azure Cache for Redis tienen un número configurable de bases de datos (valor predeterminado de 16) que se pueden usar para separar de forma lógica los datos dentro de una instancia de Azure Redis Cache. El código se conecta a la base de datos predeterminada, DB 0. Para más información, consulte What are Redis databases? (¿Qué son las bases de datos de Redis?) y Configuración predeterminada del servidor Redis.

Los elementos en la memoria caché se pueden almacenar y recuperar mediante los métodos StringSet y StringGet.

Redis almacena la mayoría de los datos como cadenas Redis, pero estas cadenas pueden contener muchos tipos de datos, como por ejemplo datos binarios serializados, que se pueden usar cuando se almacenan objetos .NET en caché.

Ejecute el siguiente comando en la ventana Comandos para compilar la aplicación:

dotnet build

Luego, ejecute la aplicación con el siguiente comando:

dotnet run

En el ejemplo siguiente, puede ver que la clave Message tenía anteriormente un valor almacenado en caché, que se estableció mediante la Consola de Redis en Azure Portal. La aplicación actualizó ese valor almacenado en caché. La aplicación también ejecutó los comandos PING y CLIENT LIST.

Aplicación de consola parcial

Trabajar con objetos .NET en la memoria caché

Azure Redis Cache puede almacenar en caché objetos .NET así como tipos de datos primitivos, pero antes de poder almacenar en caché un objeto .NET, se debe serializar. La serialización del objeto .NET es responsabilidad del desarrollador de la aplicación, que tiene total flexibilidad a la hora de elegir el serializador.

Una manera sencilla para serializar objetos es usar los métodos de serialización JsonConvert de Newtonsoft.Json y serializar a y desde JSON. En esta sección, agregará un objeto .NET a la memoria caché.

Ejecute el siguiente comando para agregar el paquete Newtonsoft.json a la aplicación:

dotnet add package Newtonsoft.json

Agregue la siguiente instrucción using al principio del archivo Program.cs:

using Newtonsoft.Json;

Agregue la siguiente definición de clase Employee a Program.cs:

class Employee
{
    public string Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }

    public Employee(string employeeId, string name, int age)
    {
        Id = employeeId;
        Name = name;
        Age = age;
    }
}

En la parte inferior del procedimiento Main() de Program.cs, antes de llamar a CloseConnection(), agregue las siguientes líneas de código para almacenar en caché y recuperar un objeto .NET serializado:

    // Store .NET object to cache
    Employee e007 = new Employee("007", "Davide Columbo", 100);
    Console.WriteLine("Cache response from storing Employee .NET object : " + 
    cache.StringSet("e007", JsonConvert.SerializeObject(e007)));

    // Retrieve .NET object from cache
    Employee e007FromCache = JsonConvert.DeserializeObject<Employee>(cache.StringGet("e007"));
    Console.WriteLine("Deserialized Employee .NET object :\n");
    Console.WriteLine("\tEmployee.Name : " + e007FromCache.Name);
    Console.WriteLine("\tEmployee.Id   : " + e007FromCache.Id);
    Console.WriteLine("\tEmployee.Age  : " + e007FromCache.Age + "\n");

Guarde Program.cs y vuelva a compilar la aplicación con el siguiente comando:

dotnet build

Ejecute la aplicación con el siguiente comando para comprobar la serialización de objetos .NET:

dotnet run

Aplicación de consola finalizada

Limpieza de recursos

Si va a seguir con el tutorial siguiente, puede mantener los recursos creados en esta guía de inicio rápido y volverlos a utilizar.

En caso contrario, si ya ha terminado con la aplicación de ejemplo de la guía de inicio rápido, puede eliminar los recursos de Azure creados en este tutorial para evitar cargos.

Importante

La eliminación de un grupo de recursos es irreversible y el grupo de recursos y todos los recursos que contiene se eliminarán de forma permanente. Asegúrese de no eliminar por accidente el grupo de recursos o los recursos equivocados. Si ha creado los recursos para hospedar este ejemplo en un grupo de recursos existente que contiene recursos que quiere conservar, puede eliminar cada recurso individualmente en la parte izquierda, en lugar de eliminar el grupo de recursos.

Inicie sesión en Azure Portal y después seleccione Grupos de recursos.

Escriba el nombre del grupo de recursos en el cuadro de texto Filtrar por nombre... . En las instrucciones de este artículo se usa un grupo de recursos llamado TestResources. En el grupo de recursos de la lista de resultados, seleccione ... y, después, Eliminar grupo de recursos.

Eliminar

Se le pedirá que confirme la eliminación del grupo de recursos. Escriba el nombre del grupo de recursos para confirmar y seleccione Eliminar.

Transcurridos unos instantes, el grupo de recursos y todos los recursos que contiene se eliminan.

Pasos siguientes

En este inicio rápido, ha aprendido a usar Azure Redis Cache desde una aplicación .NET Core. Continúe con el siguiente inicio rápido para usar Azure Redis Cache con una aplicación web ASP.NET.

¿Quiere optimizar y ahorrar en el gasto en la nube?