干掉RedisHelper,请这样用分布式缓存

本文介绍了如何在Asp.NET Core项目中弃用传统的RedisHelper,转向使用官方推荐的IDistributedCache接口及其StackExchangeRedis实现。通过依赖注入和官方库,实现Redis缓存的高效管理,并扩展接口以满足更多业务需求。

前言

我们在项目中使用Redis时通常是写一个单例模式的RedisHelper静态类,暴露一些常用的GetSet等操作,在需要使用地方直接RedisHelper.StringGet(xx,xx)就可以了,这样虽然简单粗暴地满足我们对Redis的所有操作需要,但是这在Asp.Net Core的项目显得不是那么优雅了。首先你的RedisHelper静态类无法使用Asp.Net Core容器,又如何优雅的通过依赖注入获取IConfiguration中的配置项呢?既然我们使用Asp.Net Core这么优秀的框架,最佳实践当然就是遵循官方建议的开发规范优雅的编写代码。

IDistributedCache

若要使用 SQL Server 分布式缓存,请添加对 Microsoft.Extensions.Caching.SqlServer 包的包引用。

若要使用 Redis 分布式缓存,请添加对 Microsoft.Extensions.Caching.StackExchangeRedis 包的包引用。

若要使用 NCache 分布式缓存,请添加对 NCache.Microsoft.Extensions.Caching.OpenSource 包的包引用。

无论选择哪种实现,应用都将使用 IDistributedCache 接口与缓存进行交互。

来看下IDistributedCache这个接口的定义

namespace Microsoft.Extensions.Caching.Distributed;

/// <summary>
/// Represents a distributed cache of serialized values.
/// </summary>
public interface IDistributedCache
{
    /// <summary>
    /// Gets a value with the given key.
    /// </summary>
    byte[]? Get(string key);

    /// <summary>
    /// Gets a value with the given key.
    /// </summary>
    Task<byte[]?> GetAsync(string key, CancellationToken token = default(CancellationToken));

    void Set(string key, byte[] value, DistributedCacheEntryOptions options);

    /// <summary>
    /// Sets the value with the given key.
    /// </summary>
    Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default(CancellationToken));

    /// <summary>
    /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
    /// </summary>
    void Refresh(string key);

    /// <summary>
    /// Refreshes a value in the cache based on its key, resetting its sliding expiration timeout (if any).
    /// </summary>
    Task RefreshAsync(string key, CancellationToken token = default(CancellationToken));

    /// <summary>
    /// Removes the value with the given key.
    /// </summary>
    void Remove(string key);

    /// <summary>
    /// Removes the value with the given key.
    /// </summary>
    Task RemoveAsync(string key, CancellationToken token = default(CancellationToken));
}

IDistributedCache 接口提供以下方法来处理分布式缓存实现中的项:

  • GetGetAsync:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。
  • SetSetAsync:使用字符串键将项(作为 byte[] 数组)添加到缓存。
  • RefreshRefreshAsync:根据键刷新缓存中的项,重置其可调到期超时(如果有)。
  • RemoveRemoveAsync:根据字符串键删除缓存项。

干掉RedisHelper

官方不仅提出了如何最佳实践分布式缓存的使用,还提供了基本的实现库给我们直接用,比如我们在项目中用Redis为我们提供缓存服务:

  1. 添加引用Microsoft.Extensions.Caching.StackExchangeRedis
  2. 注册容器AddStackExchangeRedisCache,并配置参数
 builder.Services.AddStackExchangeRedisCache(options =>
     {
         options.Configuration = builder.Configuration.GetConnectionString("MyRedisConStr");
         options.InstanceName = "SampleInstance";
     });
  1. 在需要使用Redis的地方通过构造函数注入IDistributedCache实例调用即可

这样就可以优雅的使用Redis了,更加符合Asp.Net Core的设计风格,养成通过容器注入的方式来调用我们的各种服务,而不是全局使用RedisHelper静态类,通过IOC的方式,结合面向接口开发,能方便的替换我们的实现类,统一由容器提供对象的创建,这种控制反转带来的好处只可意会不可言传,这里就不赘述了。

AddStackExchangeRedisCache到底干了什么

上面已经知道如何优雅的使用我们的Redis了,但是不看下源码就不知道底层实现,总是心里不踏实的。

源码比较好理解的,因为这个Nuget包的源码也就四个类,而上面注册容器的逻辑也比较简单
AddStackExchangeRedisCache主要干的活

// 1.启用Options以使用IOptions
services.AddOptions();
// 2.注入配置自定义配置,可以通过IOptions<T>注入到需要使用该配置的地方
services.Configure(setupAction);
// 3.注入一个单例IDistributedCache的实现类RedisCache
services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCache>());

所以我们在需要用Redis的地方通过构造函数注入IDistributedCache,而它对应的实现就是RedisCache,那看下它的源码。


这里就不细看所有的实现了,重点只需要知道它继承了IDistributedCache就行了,通过AddStackExchangeRedisCache传入的ConnectionString,实现IDistributedCacheGetSetRefreshRemove四个核心的方法,我相信这难不倒你,而它也就是干了这么多事情,只不过它的实现有点巧妙。

通过LUA脚本和HSET数据结构实现,HashKey是我们传入的InstanceName+key,做了一层包装。

源码中还有需要注意的就是,我们要保证Redis连接对象IConnectionMultiplexer的单例,不能重复创建多个实例,这个想必在RedisHelper中也是要保证的,而且是通过lock来实现的。

然而微软不是那么用的,玩了个花样,注意下面的_connectionLock.Wait();

private readonly SemaphoreSlim _connectionLock = new SemaphoreSlim(initialCount: 1, maxCount: 1);

[MemberNotNull(nameof(_cache), nameof(_connection))]
private void Connect()
{
    CheckDisposed();
    if (_cache != null)
    {
        Debug.Assert(_connection != null);
        return;
    }

    _connectionLock.Wait();
    try
    {
        if (_cache == null)
        {
            if (_options.ConnectionMultiplexerFactory == null)
            {
                if (_options.ConfigurationOptions is not null)
                {
                    _connection = ConnectionMultiplexer.Connect(_options.ConfigurationOptions);
                }
                else
                {
                    _connection = ConnectionMultiplexer.Connect(_options.Configuration);
                }
            }
            else
            {
                _connection = _options.ConnectionMultiplexerFactory().GetAwaiter().GetResult();
            }

            PrepareConnection();
            _cache = _connection.GetDatabase();
        }
    }
    finally
    {
        _connectionLock.Release();
    }

    Debug.Assert(_connection != null);
}

通过SemaphoreSlim限制同一时间只能有一个线程能访问_connectionLock.Wait();后面的代码。

学到装逼技巧+1

思考

IDistributedCache只有四个操作:GetSetRefreshRemove,我们表示很希望跟着官方走,但这个接口过于简单,不能满足我的其他需求咋办?
比如我们需要调用 StackExchange.Redis封装的LockTake,LockRelease来实现分布式锁的功能,那该怎么通过注入IDistributedCache调用?
我们可以理解官方上面是给我们做了示范,我们完全可以自己定义一个接口,比如:

public interface IDistributedCachePlus : IDistributedCache
{
    bool LockRelease(string key, byte[] value);

    bool LockTake(string key, byte[] value, TimeSpan expiry);
}

继承IDistributedCache,对其接口进行增强,然后自己实现实现AddStackExchangeRedisCache的逻辑,我们不用官方给的实现,但是我们山寨官方的思路,实现任意标准的接口,满足我们业务。

services.Add(ServiceDescriptor.Singleton<IDistributedCachePlus, RedisCachePlus>());

在需要使用缓存的地方通过构造函数注入IDistributedCachePlus

总结

官方提供的IDistributedCache标准及其实现类库,能方便的实现我们对缓存的简单的需求,通过遵循官方的建议,我们干掉了RedisHelper,优雅的实现了分布式Redis缓存的使用,你觉得这样做是不是很优雅呢?

### 双向循环链表的C语言实现 双向循环链表是一种特殊的链表结构,其中每个节点不仅有指向下一个节点的指针 `next`,还有指向前一个节点的指针 `prev`。这种设计使得可以在任意方向上高效地遍历整个列表[^1]。 以下是完整的双向循环链表的定义以及基本操作(创建、插入、删除和销毁)的实现: #### 定义双向循环链表节点 ```c typedef struct Node { int data; // 数据域 struct Node* prev; // 前驱指针 struct Node* next; // 后继指针 } Node; ``` #### 创建哨兵节点 为了简化边界条件处理,通常会引入一个哨兵节点作为头节点。该节点不存储有效数据,仅用于标记链表的起始位置。 ```c Node* createSentinel() { Node* sentinel = (Node*)malloc(sizeof(Node)); if (!sentinel) { perror("Memory allocation failed!"); exit(EXIT_FAILURE); } sentinel->data = -1; // 设置哨兵节点的数据为特殊值 sentinel->prev = sentinel; // 自己指向自己形成环形 sentinel->next = sentinel; return sentinel; } ``` #### 插入新节点到链表末尾 此方法会在链表的最后一个真实节点之后插入一个新的节点,并更新其前后关系以维持双向循环特性。 ```c void insertAtEnd(Node* head, int value) { Node* newNode = (Node*)malloc(sizeof(Node)); if (!newNode) { perror("Memory allocation failed!"); exit(EXIT_FAILURE); } newNode->data = value; Node* tail = head->prev; // 获取当前链表的实际尾部节点 // 更新链接关系 tail->next = newNode; newNode->prev = tail; newNode->next = head; head->prev = newNode; } ``` #### 删除指定节点 要移除某个具体节点时需小心调整周围邻居之间的连接以免破坏整体结构完整性。 ```c void deleteNode(Node* nodeToDelete) { if (nodeToDelete == NULL || nodeToDelete->next == nodeToDelete) return; // 不允许删除唯一存在的哨兵节点 Node* predecessor = nodeToDelete->prev; Node* successor = nodeToDelete->next; // 调整前后的链接 predecessor->next = successor; successor->prev = predecessor; free(nodeToDelete); // 释放被删掉的空间资源 } ``` #### 销毁整个链表 当不再需要使用某条链表的时候应该彻底清理它所占用的所有内存空间以防泄露问题发生。 ```c void destroyList(Node* head) { if (head == NULL) return; Node* current = head->next; // 开始于第一个实际元素而非哨兵本身 while (current != head) { // 循环直到回到起点即完成全部扫描 Node* temp = current; current = current->next; free(temp); } free(head); // 最终也要记得把哨兵对象给干掉才行哦~ } ``` 通过上述代码片段可以构建出一个具备基础功能的操作集合适用于大多数场景下的双向循环链表管理需求[^2][^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值