快速入門:在 .NET Framework 中使用 Azure Cache for Redis

在本快速入門中,您會將 Azure Cache for Redis 納入 .NET Framework 應用程式中,以便存取可從 Azure 內的任何應用程式存取的安全專用快取。 您會在 .NET 主控台應用程式中明確地搭配使用 StackExchange.Redis 用戶端與 C# 程式碼。

跳至 GitHub 上的程式碼

如果您想要直接跳到程式碼,請參閱 GitHub 上的.NET Framework 快速入門

必要條件

建立快取

  1. 若要建立快取,請登入 Azure 入口網站,然後選取 [建立資源] 。

    [建立資源] 在左側導覽窗格中反白顯示。

  2. 在 [新增] 頁面上選取 [資料庫] ,然後選取 [Azure Cache for Redis] 。

    在 [新增] 上,[資料庫] 和 [Azure Cache for Redis] 會反白顯示。

  3. 在 [新的 Redis 快取] 頁面上,設定新快取的設定。

    設定 選擇值 描述
    訂用帳戶 下拉並選取您的訂用帳戶。 這個新的 Azure Cache for Redis 執行個體建立所在的訂用帳戶。
    資源群組 下拉並選取資源群組,或選取 [新建] 並輸入新的資源群組名稱。 用來建立快取和其他資源的資源群組名稱。 將所有的應用程式資源放在一個資源群組中,您將可輕鬆地一併管理或刪除這些資源。
    DNS 名稱 輸入唯一名稱。 快取名稱必須是介於1到63個字元之間的字串,其中只包含數位、字母或連字號。 名稱的開頭和結尾必須是數字或字母,且不可包含連續的連字號。 您的快取執行個體 主機名稱 將是 <DNS name>.redis.cache.windows.net
    位置 下拉並選取位置。 選取其他將使用快取的服務附近的區域
    快取類型 下拉並選取階層。 快取的可用大小、效能和功能取決於階層。 如需詳細資訊,請參閱 Azure Cache for Redis 概觀
  4. 選取 [ 網路 ] 索引標籤,或選取頁面底部的 [ 網路 功能] 按鈕。

  5. 在 [網路功能] 索引標籤中,選取您的連線方法。

  6. 選取 [ 下一步: advanced ] 索引標籤,或選取頁面底部的 [ 下一步: advanced ] 按鈕。

  7. 在基本或標準快取執行個體的 [進階] 索引標籤中,如果您想要啟用非 TLS 連接埠,請選取啟用切換。 您也可以選取您想要使用的 Redis 版本,可能是4或6。

    Redis 版本 4 或 6。

  8. 在高階快取執行個體的 [進階] 索引標籤中,設定非 TLS 連接埠、叢集和資料持續性的設定。 您也可以選取您想要使用的 Redis 版本,可能是4或6。

  9. 選取 [ 下一步:標記 ] 索引標籤,或選取頁面底部的 [ 下一步:標記 ] 按鈕。

  10. 在 [標記] 索引標籤中,如果您想要分類資源,可以選擇性地輸入名稱和值。

  11. 選取 [檢閱 + 建立]。 您會移至 [檢閱 + 建立] 索引標籤,其中 Azure 會驗證您的組態。

  12. 出現綠色的「通過驗證」訊息之後,請選取 [建立]。

建立快取需要一些時間。 您可以在 Azure Cache for Redis 的 [概觀] 頁面上監視進度。 當 [狀態] 顯示為 [執行中] 時,表示快取已可供使用。

從 Azure 入口網站擷取主機名稱、連接埠和存取金鑰

若要連線至 Azure Cache for Redis 執行個體,快取用戶端需要主機名稱、連接埠和快取金鑰。 某些用戶端可能會以稍有不同的名稱來參考這些項目。 您可以從 Azure 入口網站取得主機名稱、連接埠和金鑰。

  • 若要取得存取金鑰,請從快取的左側導覽中選取 [存取金鑰] 。

    Azure Cache for Redis 金鑰

  • 若要取得主機名稱和連接埠,請從快取的左側導覽中選取 [屬性] 。 主機名稱的格式為 <DNS name>.redis.cache.windows.net

    Azure Redis 快取屬性

在電腦上建立名為 CacheSecrets.config 的檔案,並將它放在應用程式範例的原始程式碼不會簽入的位置。 在本快速入門中,CacheSecrets.config 檔案位於這裡 (C:\AppSecrets\CacheSecrets.config) 。

編輯 CacheSecrets.config 檔案,並新增下列內容:

<appSettings>
    <add key="CacheConnection" value="<host-name>,abortConnect=false,ssl=true,allowAdmin=true,password=<access-key>"/>
</appSettings>

<host-name> 取代為快取主機名稱。

<access-key> 取代為快取的主要金鑰。

建立主控台應用程式

在 Visual Studio 中,選取 [檔案] > [新增] > [專案]。

選取 [主控台應用程式 (.NET Framework)] ,然後選取 [下一步] 來設定您的應用程式。 輸入 Project 名稱,確認已選取 .NET Framework 4.6.1 或更高版本,然後選取 [建立] 以建立新的主控台應用程式。

設定快取用戶端

在本節中,您會設定主控台應用程式,以使用適用於 .NET 的 StackExchange.Redis 用戶端。

在 Visual Studio 中,選取 [工具 > ]NuGet 封裝管理員 > 封裝管理員主控台],然後從封裝管理員主控台視窗執行下列命令。

Install-Package StackExchange.Redis

安裝完成後,StackExchange.Redis 快取用戶端即可與專案搭配使用。

連接到快取

在 Visual Studio 中開啟 App.config 檔案,並將其更新為包含 appSettings file 屬性 (會參考 CacheSecrets.config 檔案)。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.7.2" />
    </startup>

    <appSettings file="C:\AppSecrets\CacheSecrets.config"></appSettings>
</configuration>

在方案總管中,以滑鼠右鍵按一下 [ 參考 ],然後選取 [ 加入參考]。 新增 System.Configuration 組件的參考。

將下列 using 陳述式新增至 Program.cs :

using StackExchange.Redis;
using System.Configuration;

與 Azure Cache for Redis 的連線會由 ConnectionMultiplexer 類別所管理。 整個用戶端應用程式中都應該共用和重複使用此類別。 請勿對每個作業建立新連線。

請勿將認證儲存在原始程式碼中。 為了簡化這個範例,我只使用外部密碼組態檔。 搭配使用 Azure 金鑰保存庫與憑證會更好。

Program.cs 中,對主控台應用程式的 Program 類別新增下列成員:

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 = ConfigurationManager.AppSettings["CacheConnection"].ToString();
        return ConnectionMultiplexer.Connect(cacheConnection);
    });
}

這個在應用程式中共用 ConnectionMultiplexer 執行個體的方法,會使用可傳回已連線執行個體的靜態屬性。 此程式碼會提供安全執行緒方式,只初始化一個已連線的 ConnectionMultiplexer 執行個體。 abortConnect 會設為 false,這表示即使無法建立與 Azure Cache for Redis 的連線,呼叫也會成功。 ConnectionMultiplexer 的主要功能之一,就是一旦網路問題或其他原因獲得解決,它就會自動恢復與快取的連接。

CacheConnection appSetting 的值可用來從 Azure 入口網站中將快取連接字串參考作為密碼參數。

重新連接來處理 RedisConnectionException 和 >socketexception

在上呼叫方法時,建議的最佳作法 ConnectionMultiplexer 是藉 RedisConnectionException SocketException 由關閉並重新建立連接來嘗試自動解析和例外狀況。

將下列 using 陳述式新增至 Program.cs :

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

Program .cs 中,將下列成員新增至 Program 類別:

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.");
    }
    _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 = ConfigurationManager.AppSettings["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));
}

執行快取命令

對主控台應用程式 Program 類別的 Main 程序新增下列程式碼:

static void Main(string[] args)
{
    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 console app!\"";
    Console.WriteLine("\nCache command  : " + cacheCommand + " or StringSet()");
    Console.WriteLine("Cache response : " + cache.StringSet("Message", "Hello! The cache is working from a .NET 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);
}

Azure Cache for Redis 具有可用來以邏輯方式區隔 Azure Cache for Redis 內資料的可設定資料庫數目 (預設值為 16 個)。 該程式碼會連線至預設資料庫 (DB 0)。 如需詳細資訊,請參閱 Redis 資料庫是什麼?預設 Redis 伺服器組態

您可以使用 StringSetStringGet 方法來儲存和擷取快取項目。

Redis 會將多數資料儲存為 Redis 字串,但這些字串可能包含許多類型的資料,包括序列化的二進位資料 (在快取中儲存 .NET 物件時可能會用到)。

Ctrl+F5 以建置並執行主控台應用程式。

在下列範例中,您會看到 Message 金鑰先前有快取值,此值是在 Azure 入口網站中使用 Redis 主控台所設定的。 應用程式更新了該快取值。 應用程式也已執行 PINGCLIENT LIST 命令。

主控台應用程式部分

使用快取中的 .NET 物件

Azure Cache for Redis 可以快取 .NET 物件及基本資料類型,但必須先將 .NET 物件序列化,才能加以快取。 .NET 物件序列化是應用程式開發人員的責任,同時賦與開發人員選擇序列化程式的彈性。

將物件序列化的其中一個簡單方法就是使用 Newtonsoft.Json 中的 JsonConvert 序列化方法並進行 JSON 的雙向序列化。 在本節中,您會對快取新增 .NET 物件。

在 Visual Studio 中,選取 [工具 > ]NuGet 封裝管理員 > 封裝管理員主控台],然後從封裝管理員主控台視窗執行下列命令。

Install-Package Newtonsoft.Json

Program.cs 開頭處新增下列 using 陳述式:

using Newtonsoft.Json;

將下列 Employee 類別定義新增至 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;
    }
}

Program.cs 中的 Main() 程序底部,於 CloseConnection() 呼叫之前,對快取新增下列程式碼,並擷取已序列化的 .NET 物件:

    // 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");

Ctrl+F5 以建置並執行主控台應用程式,來測試 .NET 物件的序列化。

主控台應用程式已完成

清除資源

如果您準備繼續進行下一個教學課程,則可以保留在本快速入門中所建立的資源,並重複使用它們。

否則,如果您已完成快速入門範例應用程式,便可以將在此快速入門中所建立的 Azure 資源刪除,以避免衍生費用。

重要

刪除資源群組是無法回復的動作,資源群組和其內的所有資源將會永久刪除。 請確定您不會不小心刪除錯誤的資源群組或資源。 如果您在包含您想要保留之資源的現有資源群組內建立用來裝載此範例的資源,則可以在左側個別刪除每個資源,而不是刪除資源群組。

登入 Azure 入口網站,然後選取 [資源群組] 。

在 [依名稱篩選...] 文字方塊中,輸入您的資源群組名稱。 本文的指示是使用名為 TestResources 的資源群組。 在結果清單中的目標資源群組上方,選取 ... ,然後按一下 [刪除資源群組] 。

刪除

系統將會要求您確認是否刪除資源。 輸入您資源群組的名稱以進行確認,然後選取 [刪除]。

片刻過後,系統便會刪除該資源群組及其所有內含的資源。

後續步驟

在本快速入門中,您已了解如何從 .NET 應用程式使用 Azure Cache for Redis。 請繼續閱讀下一個快速入門,以搭配使用 Azure Cache for Redis 與 ASP.NET Web 應用程式。

想要最佳化並節省您的雲端費用嗎?