.NetCore下基於FreeRedis實現的Redis6.0客戶端緩存之緩存鍵條件優雅過濾

来源:https://www.cnblogs.com/simendancer/archive/2023/01/15/17052784.html
-Advertisement-
Play Games

前言 眾所周知記憶體緩存(MemoryCache)數據是從記憶體中獲取,性能表現上是最優的,但是記憶體緩存有一個缺點就是不支持分散式,數據在各個部署節點上各存一份,每份緩存的過期時間不一致,會導致幻讀等各種問題,所以我們實現分散式緩存通常會用上Redis 但如果在高併發的情況下讀取Redis的緩存,會進行 ...


前言

眾所周知記憶體緩存(MemoryCache)數據是從記憶體中獲取,性能表現上是最優的,但是記憶體緩存有一個缺點就是不支持分散式,數據在各個部署節點上各存一份,每份緩存的過期時間不一致,會導致幻讀等各種問題,所以我們實現分散式緩存通常會用上Redis

但如果在高併發的情況下讀取Redis的緩存,會進行頻繁的網路I/O,假如有一些不經常變動的熱點緩存,這不就會白白浪費了帶寬,並且讀到數據以後可能還需要進行反序列化,還影響了CPU性能,造成資源的浪費

從Redis 6.0開始有一個重要特性就是支持客戶端緩存(僅支持String類型),效果跟記憶體緩存是一樣的,數據都是從記憶體中獲取,如果服務端緩存數據發送變動,會在極短的時間內通知到所有客戶端進行數據同步

在 .NetCore 環境中,我們常用的Redis組件是 StackExchangeRedis 和 CSRedisCore,但是都不支持6.0的客戶端緩存這一特性,CSRedisCore 的作者在前兩年又重新開發了一個叫 FreeRedis 的組件,並支持了客戶端緩存

我們當時為了實現某個對性能有較高要求的產品需求,但不想額外增加硬體上的資源,急需使用上這一特性,在調研後發現了這個組件,經過測試後發現沒什麼問題就直接用上了

不過我們的主力組件還是CSRedisCore,FreeRedis基本只是用到了客戶端緩存,因為當時的版本還不支持非同步方法,我記得是今年才加上的

FreeRedis組件介紹原文,有關客戶端緩存具體實現原理看看這篇就夠了:FreeRedis

目前FreeRedis在我司項目中也已經穩定運行了一年多,這裡分享一下我們在項目中的實際用法

 

擴展前

為什麼要擴展?因為當看過官方的Demo以後,其中讓我比較難受的是本地緩存鍵的過濾條件設置

 

我想到的有三種方式配置這個條件

第一種:在具體實現某個緩存的地方,才設置過濾條件

缺點:

每次都得寫一遍有點冗餘,而且查看源碼可以發現UseClientSideCaching這個方法每次都會實例一個叫ClientSideCachingContext的類,併在裡面添加訂閱、添加攔截器等一系列操作

這種方式我測試過,雖然每次都調用一下不影響最後客戶端緩存效果,但RedisClient中的攔截器是一直在新增的,這上線後不得崩了?

所以意味具體業務實現代碼中每次還實現一下不重覆調用UseClientSideCaching的特殊邏輯,即使實現了,但每個不重覆的Key都會往RedisClient新增一個攔截器,極力不推薦這種方式!

 

第二種:在同一個地方把所有需要進行本地緩存的鍵一口氣設置好過濾條件

缺點:

時間長了以後,這裡會寫得非常的長,非常的醜陋,而且你並不知道哪些鍵已經廢棄以及對應的業務

當然項目是從頭到尾是你一個人負責開發的或需要本地緩存的Key並不多的時候,這種方式其實也夠了

 

第三種:所有用到客戶端緩存的鍵約定好一個統一命名首碼,那麼過濾條件這裡只需要寫一個 StartWith(命名首碼) 的條件就行了

缺點:

需要給團隊提前培訓下這個註意項,但是時間長了以後,大伙完全不知道後面匹配的那麼多鍵對應是什麼業務

某些業務可能一口氣需要用到了好幾個緩存Key組合進行實現,但其中只有一個Key需要本地緩存,那麼這個Key的首碼和其他Key的業務命名首碼就不統一了,雖然沒什麼問題,但是在客戶端工具中查看鍵值時沒放在一起,不利於查找

在Key不多且項目參與人數不多的情況下,用這個方式是最簡單方便的

 

 三種方式在實現好用程度上排個序: 第三種 > 第二種 > 第一種

 

擴展後

三種方式在我司項目中其實都不好用,我們項目中之前的所有緩存都是一個緩存實現對應一個緩存類,每個緩存類會繼承一個對應該緩存用的Redis數據結構基類,例如CacheBaseStringCacheBaseSetCacheBaseSortedSetCacheBaseList...等

基類中已經實現好了對應數據結構通用的方法,例如CacheBaseString中已經實現了Get Set Del Expire這樣的通用方法,在派生的緩存類中只要重寫基類的抽象方法,設置下Key的命名緩存過期時間,一個緩存實現就結束了,這樣便於管理和使用,團隊的小伙伴幾年來也都習慣了這種用法

所以基於這個要求,我們對FreeRedis的客戶端緩存實現進行一下擴展,首先客戶端緩存只支持String類型,所以就是再寫一個String結構的ClientSideCacheBase就好了,最麻煩的就是如何優雅的統一實現Key的過濾條件

可以發現UseClientSideCaching中KeyFilter是個Lambda Func委托,返回一個布爾值

 

那麼我馬上想到的就是表達式樹,我們在各種高度封裝的ORM中經常能看到使用表達式樹去組裝SQL的Where條件

同樣的原理,我們也可以通過在項目啟動時通過反射拿到所有派生類,並調用基類中的一個抽象方法,最後合併表達樹,返回一個Func給這個KeyFilter

1. 首先我們先設計一下基類

其中核心的兩個方法就是 Key的抽象過濾條件的抽象,其中的 FreeRedisService 是已經實現好的一個FreeRedisClient,需要在IOC容器中註入為單例,所以在這基類的構造函數中,必須傳入IServiceProvider,從容器拿到FreeRedisService實例才能實現下麵那些通用方法

    /// <summary>
    /// Redis6.0客戶端緩存實現基類
    /// </summary>
    public abstract class ClienSideCacheBase
    {
        /// <summary>
        /// RedisService
        /// </summary>
        private static FreeRedisService _redisService;

        /// <summary>
        /// 獲取RedisKey
        /// </summary>
        /// <returns></returns>
        protected abstract string GetRedisKey();

        /// <summary>
        /// 設置客戶端緩存Key過濾條件
        /// </summary>
        /// <returns></returns>
        public abstract Expression<Func<string,bool>> SetCacheKeyFilter();

        /// <summary>
        /// 私有構造函數
        /// </summary>
        private ClienSideCacheBase() { }

        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="serviceProvider"></param>
        public ClienSideCacheBase(IServiceProvider serviceProvider)
        {
            _redisService = serviceProvider.GetService<FreeRedisService>();
        }

        /// <summary>
        /// 獲取值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public T Get<T>()
        {
            return _redisService.Instance.Get<T>(GetRedisKey());
        }

        /// <summary>
        /// 設置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <returns></returns>
        public bool Set<T>(T data)
        {
            _redisService.Instance.Set(GetRedisKey(),data);
            return true;
        }

        /// <summary>
        /// 設置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="seconds"></param>
        /// <returns></returns>
        public bool Set<T>(T data,int seconds)
        {
            _redisService.Instance.Set(GetRedisKey(),data,TimeSpan.FromSeconds(seconds));
            return true;
        }

        /// <summary>
        /// 設置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="expired"></param>
        /// <returns></returns>
        public bool Set<T>(T data,TimeSpan expired)
        {
            _redisService.Instance.Set(GetRedisKey(),data,expired);
            return true;
        }

        /// <summary>
        /// 設置值
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="data"></param>
        /// <param name="expiredAt"></param>
        /// <returns></returns>
        public bool Set<T>(T data,DateTime expiredAt)
        {
            _redisService.Instance.Set(GetRedisKey(),data,TimeSpan.FromSeconds(expiredAt.Subtract(DateTime.Now).TotalSeconds));
            return true;
        }

        /// <summary>
        /// 設置過期時間
        /// </summary>
        /// <returns></returns>
        public bool SetExpire(int seconds)
        {
            return _redisService.Instance.Expire(GetRedisKey(),TimeSpan.FromSeconds(seconds));
        }

        /// <summary>
        /// 設置過期時間
        /// </summary>
        /// <returns></returns>
        public bool SetExpire(TimeSpan expired)
        {
            return _redisService.Instance.Expire(GetRedisKey(),expired);
        }

        /// <summary>
        /// 設置過期時間
        /// </summary>
        /// <returns></returns>
        public bool SetExpireAt(DateTime expiredTime)
        {
            return _redisService.Instance.ExpireAt(GetRedisKey(),expiredTime);
        }

        /// <summary>
        /// 移除緩存
        /// </summary>
        /// <returns></returns>
        public long Remove()
        {
            return _redisService.Instance.Del(GetRedisKey());
        }

        /// <summary>
        /// 緩存是否存在
        /// </summary>
        /// <returns></returns>
        public bool Exists()
        {
            return _redisService.Instance.Exists(GetRedisKey());
        }
    }

具體繼承用法如下:

    /// <summary>
    /// 實現客戶端緩存Demo1
    /// </summary>
    public class ClientSideDemoOneCache : ClienSideCacheBase
    {
        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="serviceProvider"></param>
        public ClientSideDemoOneCache(IServiceProvider serviceProvider) : base(serviceProvider) { }

        /// <summary>
        /// 設置Key過濾規則
        /// </summary>
        /// <returns></returns>
        public override Expression<Func<string,bool>> SetCacheKeyFilter()
        {
            return o => o == GetRedisKey();
        }

        /// <summary>
        /// 獲取緩存的Key
        /// </summary>
        /// <returns></returns>
        protected override string GetRedisKey()
        {
            return "DemoOneRedisKey";
        }
    }
    
    /// <summary>
    /// 實現客戶端緩存Demo2
    /// </summary>
    public class ClientSideDemoTwoCache : ClienSideCacheBase
    {
        /// <summary>
        /// 構造函數
        /// </summary>
        /// <param name="serviceProvider"></param>
        public ClientSideDemoTwoCache(IServiceProvider serviceProvider) : base(serviceProvider) { }

        /// <summary>
        /// 設置Key過濾規則
        /// </summary>
        /// <returns></returns>
        public override Expression<Func<string,bool>> SetCacheKeyFilter()
        {
            return o => o.StartsWith(GetRedisKey());
        }

        /// <summary>
        /// 獲取緩存的Key
        /// </summary>
        /// <returns></returns>
        protected override string GetRedisKey()
        {
            return "DemoTwoRedisKey";
        }
    }

 

2. FreeRedisService的實現

其中關鍵代碼就是一次性設置好項目中所有本地緩存的過濾條件,FreeRedisService最終會註冊為一個單例

    public class FreeRedisService
    {
        /// <summary>
        /// RedisClient
        /// </summary>
        private static RedisClient _redisClient;

        /// <summary>
        /// 初始化配置
        /// </summary>
        private FreeRedisOption _redisOption;

        /// <summary>
        /// 構造函數
        /// </summary>
        public FreeRedisService(FreeRedisOption redisOption)
        {
            if (redisOption == null) {
                throw new NullReferenceException("初始化配置為空");
            }
            _redisOption = redisOption;
            InitRedisClient();
        }

        /// <summary>
        /// 懶載入Redis客戶端
        /// </summary>
        private readonly static Lazy<RedisClient> redisClientLazy = new Lazy<RedisClient>(() => {
            var r = _redisClient;
            r.Serialize = obj => JsonConvert.SerializeObject(obj);
            r.Deserialize = (json,type) => JsonConvert.DeserializeObject(json,type);
            r.Notice += (s,e) => Console.WriteLine(e.Log);
            return r;
        });

        private static readonly object obj = new object();

        /// <summary>
        /// 初始化Redis
        /// </summary>
        /// <returns></returns>
        bool InitRedisClient()
        {
            if (_redisClient == null) {
                lock (obj) {
                    if (_redisClient == null) {
                        _redisClient = new RedisClient($"{_redisOption.RedisHost}:{_redisOption.RedisPort},password={_redisOption.RedisPassword},defaultDatabase={_redisOption.DefaultIndex},poolsize={_redisOption.Poolsize},ssl=false,writeBuffer=10240,prefix={_redisOption.Prefix},asyncPipeline={_redisOption.asyncPipeline},connectTimeout={_redisOption.ConnectTimeout},abortConnect=false");
                        //設置客戶端緩存
                        if (_redisOption.UseClientSideCache) {
                            if (_redisOption.ClientSideCacheKeyFilter == null) {
                                throw new NullReferenceException("如果開啟客戶端緩存,必須設置客戶端緩存Key過濾條件");
                            }
                            _redisClient.UseClientSideCaching(new ClientSideCachingOptions() {
                                Capacity = 0,  //本地緩存的容量,0不限制
                                KeyFilter = _redisOption.ClientSideCacheKeyFilter,  //過濾哪些鍵能被本地緩存
                                CheckExpired = (key,dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(3)  //檢查長期未使用的緩存
                            });
                        }
                        return true;
                    }
                }
            }
            return _redisClient != null;
        }

        /// <summary>
        /// 獲取Client實例
        /// </summary>
        public RedisClient Instance {
            get {
                if (InitRedisClient()) {
                    return redisClientLazy.Value;
                }
                throw new NullReferenceException("Redis不可用");
            }
        }
    }

 

3. 反射遍歷獲取所有過濾條件

我們寫一個反射的方法,去遍歷所有的緩存派生類,並調用其中重寫過的過濾條件抽象方法,最後合併為一個表達式樹,Or這個方法是一個自定義擴展方法,具體看Github完整項目

    /// <summary>
    /// 構建Redis客戶端緩存Key條件
    /// </summary>
    public class ClientSideCacheKeyBuilder
    {
        /// <summary>
        /// 具體緩存業務實現所在項目程式集
        /// </summary>
        const string DefaultDllName = "Hy.Components.Api";

        /// <summary>
        /// 構建表達式樹
        /// </summary>
        /// <param name="serviceProvider">serviceProvider</param>
        /// <param name="dllName">當前類所在的項目dll名</param>
        /// <returns></returns>
        public static Func<string,bool> Build(IServiceProvider serviceProvider,string dllName = DefaultDllName)
        {
            Expression<Func<string,bool>> expression = o => false; //預設false
            var baseClass = typeof(ClienSideCacheBase);
            Assembly ass = Assembly.LoadFrom($"{AppDomain.CurrentDomain.BaseDirectory}{dllName}.dll");
            Type[] types = ass.GetTypes();
            foreach (Type item in types) {
                if (item.IsInterface || item.IsEnum || item.GetCustomAttribute(typeof(ObsoleteAttribute)) != null) {
                    continue;
                }
                //判讀基類
                if (item != null && item.BaseType == baseClass) {
                    var instance = (ClienSideCacheBase)Activator.CreateInstance(item,serviceProvider); //這裡參數帶入IServiceProvider純粹為了創建實例不報錯
                    var expr = instance.SetCacheKeyFilter();
                    expression = expression.Or(expr); //合併樹
                }
            }
            return expression.Compile();
        }
    }

 

4. 將FreeRedis服務在IOC容器中註入

我們在項目啟動時,調用上面的Build方法,將返回的Func委托傳入到FreeRedisService中即可,這裡我是寫了一個IServiceCollection的擴展方法

    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// ServiceInject
        /// </summary>
        /// <param name="services"></param>
        public static void AddRedisService(this IServiceCollection services,IConfiguration configuration)
        {
            var clientCacheKeyFilter = ClientSideCacheKeyBuilder.Build(services.BuildServiceProvider()); //構造過濾條件
            var option = GetRedisOption(configuration,clientCacheKeyFilter); //組裝Redis初始配置
            services.AddSingleton(c => new FreeRedisService(option)); //FreeRedis註入為單例
        }

        /// <summary>
        /// 獲取配置
        /// </summary>
        /// <param name="configuration"></param>
        /// <param name="clientSideCacheKeyFilter"></param>
        /// <returns></returns>
        static FreeRedisOption GetRedisOption(IConfiguration configuration,Func<string,bool> clientSideCacheKeyFilter = null)
        {
            return new FreeRedisOption() {
                RedisHost = configuration.GetSection("Redis:RedisHost").Value,
                RedisPassword = configuration.GetSection("Redis:RedisPassword").Value,
                RedisPort = Convert.ToInt32(configuration.GetSection("Redis:RedisPort").Value),
                SyncTimeout = 5000,
                ConnectTimeout = 15000,
                DefaultIndex = 0,
                Poolsize = 5,
                UseClientSideCache = clientSideCacheKeyFilter != null,
                ClientSideCacheKeyFilter = clientSideCacheKeyFilter
            };
        }
    }

在項目IOC容器中註入,以下為.Net6的Program模板

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddHealthChecks();

//註入Redis服務
builder.Services.AddRedisService(builder.Configuration);

//可選:註入客戶端緩存具體實現類。 如果實現有很多,這裡會有一大堆註入代碼。在代碼中直接實例化類並傳入IServiceProvider也一樣的
builder.Services.AddSingleton<ClientSideDemoOneCache>();
builder.Services.AddSingleton<ClientSideDemoTwoCache>();

//構建WebApplication
var app = builder.Build();

app.UseAuthorization();

app.MapControllers();

app.UseHealthChecks("/health");

app.Run();

 

5. 最後看下我們在業務代碼中的具體用法

其中的ClientSideDemoOneCache這個實例,我們可以通過直接實例化並傳入IServiceProvider的方式使用,也可以通過構造函數註入,前提是在上面IOC容器中註入過了

    [ApiController]
    [Route("[controller]")]
    public class HomeController : ControllerBase
    {
        private readonly ILogger<HomeController> _logger;
        private readonly IServiceProvider _serviceProvider;
        private readonly ClientSideDemoOneCache _clientSideDemoOneCache;

        public HomeController(ILogger<HomeController> logger,IServiceProvider serviceProvider,ClientSideDemoOneCache clientSideDemoOneCache)
        {
            _logger = logger;
            _serviceProvider = serviceProvider;
            _clientSideDemoOneCache = clientSideDemoOneCache;
        }

        #region 可通過啟動不同埠的Api,分別調用以下介面對同一個Key進行操作,測試客戶端緩存是否生效以及是否及時同步

        /// <summary>
        /// 測試get
        /// </summary>
        /// <returns></returns>
        [HttpGet, Route("getvalue")]
        public string TestGetValue()
        {
            ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
            //cacheOne = _clientSideDemoOneCache; //通過容器拿到實例
            var value = cacheOne.Get<string>();
            return value ?? "緩存空了";
        }

        /// <summary>
        /// 測試set
        /// </summary>
        /// <param name="value"></param>
        /// <returns></returns>
        [HttpGet, Route("setvalue")]
        public string TestSetValue([FromQuery] string value)
        {
            ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
            cacheOne.Set(value);
            return "OK";
        }

        /// <summary>
        /// 測試del
        /// </summary>
        /// <returns></returns>
        [HttpGet, Route("delvalue")]
        public string TestDelValue()
        {
            ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
            cacheOne.Remove();
            return "OK";
        }

        #endregion
    }

 

6. 單機測試

1. 啟動項目看一下,先設置一個值,可以看到在Redis中已經添加成功

 

Redis客戶端:

 

2. 再獲取一下值,成功拿到

 

3. 再次刷新一下,我們看下列印出來的日誌,可以發現第一次是從服務端取值,第二次顯示從本地取值,說明過濾條件已經生效了

 

 7. 在本機開啟兩個Api服務,模擬分散式測試

1. 通過2個不同的埠啟動兩個Api服務,可以看到目前拿到都是同一個值

2. 我們通過其中一個服務修改一下值,發現另外一邊馬上就變化了

3. 再次刷新一下getvalue介面,看下日誌,發現第一次的值222222是從服務端獲取,第二次又是從本地獲取了

4. 接著我們再通過其中一個服務,刪掉這個Key,發現另一邊馬上就獲取不到值了

 

以上的完整代碼已經放到Github上查看完整代碼

 

原創作者:Harry

原文出處:https://www.cnblogs.com/simendancer/articles/17052784.html


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 引入我們前面使用過了輸出語句System.out.println();知道了它能夠輸出( )里的東西但是它到底能夠輸出一些什麼東西呢,或者直奔主題它能夠輸出什麼類型呢可以嘗試一下,如果我們輸入的直接是abcd,那麼顯然不行,它已經在報紅了,編譯器根本不認識它,表示錯誤你不能這樣子寫再嘗試一下,我們之 ...
  • Spring基本介紹 1.官方資料和下載 1.1Spring5下載 直接訪問 https://repo.spring.io/ui/native/release/org/springframework/spring/,選擇相應版本即可 進入官網 https://spring.io/ 進入Spring5 ...
  • /** * @function 動態的設置env文件中某項配置值 * @param $env_path string env文件路徑 * @param $key string 配置項 * @param $val string|int 配置值 * @return bool 返回是否成功修改 * @ot ...
  • StringBuffer類 一、 結構剖析 Java.lang.StringBuffer 代表可變的字元序列,可以對字元串內容進行增刪。 很多方法與String相同,但StringBuffer是可變長度的。 StringBuffer是一個容器。 String VS StringBuffer Stri ...
  • 本文使用代碼 book_dict = {"price": 500, "bookName": "Python設計", "weight": "250g"} 第一種方式:使用[] book_dict["owner"] = "tyson" 說明:中括弧指定key,賦值一個value,key不存在,則是添加元 ...
  • 第二章 線程管控 主要內容: 啟動線程,並通過幾種方式為新線程指定運行代碼 等待線程完成和分離線程並運行 唯一識別一個線程 2.1 線程的基本管控 ​ main函數其本聲就是一個線程,在其中又可以啟動別的線程和設置其對應的函數入口。 2.1.1 發起線程 ​ 不管線程要執行的任務是複雜還是簡單,其最 ...
  • 作者:Hai Xiang 來源:https://www.cnblogs.com/haixiang/p/12867160.html 什麼是elasticsearch Elasticsearch 是一個開源的高度可擴展的全文搜索和分析引擎,擁有查詢近實時的超強性能。 大名鼎鼎的Lucene 搜索引擎被廣 ...
  • 1 簡介 GKE(Google Kubernetes Engine)是一個K8s平臺, 我們可以使用gcloud來創建GKE集群。在開始之前,可以查看:《初始化一個GCP項目並用gcloud訪問操作》。 2 創建GKE集群 2.1 打開API 在創建集群之前,需要打開Google API,不然無法操 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...