【.NET Core項目實戰-統一認證平臺】第十五章 網關篇-使用二級緩存提升性能

来源:https://www.cnblogs.com/jackcao/archive/2019/03/05/10478425.html
-Advertisement-
Play Games

" 【.NET Core項目實戰 統一認證平臺】開篇及目錄索引 " 一、背景 首先說聲抱歉,可能是因為假期綜合症(其實就是因為懶哈)的原因,已經很長時間沒更新博客了,現在也調整的差不多了,準備還是以每周1 2篇的進度來更新博客,並完成本項目所有功能。 言歸正傳,本重構項目是在我根據實際需求重構,由於 ...


【.NET Core項目實戰-統一認證平臺】開篇及目錄索引

一、背景

首先說聲抱歉,可能是因為假期綜合症(其實就是因為懶哈)的原因,已經很長時間沒更新博客了,現在也調整的差不多了,準備還是以每周1-2篇的進度來更新博客,並完成本項目所有功能。

言歸正傳,本重構項目是在我根據實際需求重構,由於還未完全寫完,所以也沒進行壓測,在2月份時,張善友老師給我留言說經過壓測發現我重構的Ocelot網關功能性能較差,其中根本原因就是緩存模塊,由於重構項目的緩存強依賴Redis緩存,造成性能瓶頸,發現問題後,我也第一時間進行測試,性能影響很大,經過跟張老師請教,可以使用二級緩存來解決性能問題,首先感謝張老師關註並指點迷津,於是就有了這篇文章,如何把現有緩存改成二級緩存並使用。

二、改造思路

為瞭解決redis的強依賴性,首先需要把緩存數據存儲到本地,所有請求都優先從本地提取,如果提取不到再從redis提取,如果redis無數據,在從資料庫中提取。提取流程如下:

MemoryCache > Redis > db

此種方式減少提取緩存的網路開銷,也合理利用了分散式緩存,並最終減少資料庫的訪問開銷。但是使用此種方案也面臨了一個問題是如何保證集群環境時每個機器本地緩存數據的一致性,這時我們會想到redis的發佈、訂閱特性,在數據發生變動時更新redis數據併發布緩存更新通知,由每個集群機器訂閱變更事件,然後處理本地緩存記錄,最終達到集群緩存的緩存一致性。

但是此方式對於緩存變更非常頻繁的業務不適用,比如限流策略(準備還是使用分散式redis緩存實現),但是可以擴展配置單機限流時使用本地緩存實現,如果誰有更好的實現方式,也麻煩告知下集群環境下限流的實現,不勝感激。

三、改造代碼

首先需要分析下目前改造後的Ocelot網關在哪些業務中使用的緩存,然後把使用本地緩存的的業務重構,增加提取數據流程,最後提供網關外部緩存初始化介面,便於與業務系統進行集成。

1.重寫緩存方法

找到問題的原因後,就可以重寫緩存方法,增加二級緩存支持,預設使用本地的緩存,新建CzarMemoryCache類,來實現IOcelotCache<T>方法,實現代碼如下。

using Czar.Gateway.Configuration;
using Czar.Gateway.RateLimit;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Cache;
using System;

namespace Czar.Gateway.Cache
{
    /// <summary>
    /// 金焰的世界
    /// 2019-03-03
    /// 使用二級緩存解決集群環境問題
    /// </summary>
    public class CzarMemoryCache<T> : IOcelotCache<T>
    {
        private readonly CzarOcelotConfiguration _options;
        private readonly IMemoryCache _cache;
        public CzarMemoryCache(CzarOcelotConfiguration options,IMemoryCache cache)
        {
            _options = options;
            _cache = cache;
        }
        public void Add(string key, T value, TimeSpan ttl, string region)
        {
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix,region, key);
            if (_options.ClusterEnvironment)
            {
                var msg = value.ToJson();
                if (typeof(T) == typeof(CachedResponse))
                {//帶過期時間的緩存
                    _cache.Set(key, value, ttl); //添加本地緩存
                    RedisHelper.Set(key, msg); //加入redis緩存
                    RedisHelper.Publish(key, msg); //發佈
                }
                else if (typeof(T) == typeof(CzarClientRateLimitCounter?))
                {//限流緩存,直接使用redis
                    RedisHelper.Set(key, value, (int)ttl.TotalSeconds);
                }
                else
                {//正常緩存,發佈
                    _cache.Set(key, value, ttl); //添加本地緩存
                    RedisHelper.Set(key, msg); //加入redis緩存
                    RedisHelper.Publish(key, msg); //發佈
                }
            }
            else
            {
                _cache.Set(key, value, ttl); //添加本地緩存
            }
        }

        public void AddAndDelete(string key, T value, TimeSpan ttl, string region)
        {
            Add(key, value, ttl, region);
        }

        public void ClearRegion(string region)
        {
            if (_options.ClusterEnvironment)
            {
                var keys = RedisHelper.Keys(region + "*");
                RedisHelper.Del(keys);
                foreach (var key in keys)
                {
                    RedisHelper.Publish(key, ""); //發佈key值為空,處理時刪除即可。
                }
            }
            else
            {
                _cache.Remove(region);
            }
        }

        public T Get(string key, string region)
        {
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            if(region== CzarCacheRegion.CzarClientRateLimitCounterRegion&& _options.ClusterEnvironment)
            {//限流且開啟了集群支持,預設從redis取
                return RedisHelper.Get<T>(key);
            }
            var result = _cache.Get<T>(key);
            if (result == null&& _options.ClusterEnvironment)
            {
                result= RedisHelper.Get<T>(key);
                if (result != null)
                {
                    if (typeof(T) == typeof(CachedResponse))
                    {//查看redis過期時間
                        var second = RedisHelper.Ttl(key);
                        if (second > 0)
                        {
                            _cache.Set(key, result, TimeSpan.FromSeconds(second));
                        }
                    }
                    else
                    {
                        _cache.Set(key, result, TimeSpan.FromSeconds(_options.CzarCacheTime));
                    }
                }
            }
            return result;
        }
    }
}

上面就段代碼實現了本地緩存和Redis緩存的支持,優先從本地提取,如果在集群環境使用,增加redis緩存支持,但是此種方式不適用緩存變更非常頻繁場景,比如客戶端限流的實現,所以在代碼中把客戶端限流的緩存直接使用redis緩存實現。

2.註入實現和訂閱

有了實現代碼後,發現還缺少添加緩存註入和配置信息修改。首先需要修改配置文件來滿足是否開啟集群判斷,然後需要實現redis的不同部署方式能夠通過配置文件配置進行管理,避免硬編碼導致的不可用問題。

配置文件CzarOcelotConfiguration.cs修改代碼如下:

namespace Czar.Gateway.Configuration
{
    /// <summary>
    /// 金焰的世界
    /// 2018-11-11
    /// 自定義配置信息
    /// </summary>
    public class CzarOcelotConfiguration
    {
        /// <summary>
        /// 資料庫連接字元串,使用不同資料庫時自行修改,預設實現了SQLSERVER
        /// </summary>
        public string DbConnectionStrings { get; set; }

        /// <summary>
        /// 金焰的世界
        /// 2018-11-12
        /// 是否啟用定時器,預設不啟動
        /// </summary>
        public bool EnableTimer { get; set; } = false;

        /// <summary>
        /// 金焰的世界
        /// 2018-11.12
        /// 定時器周期,單位(毫秒),預設30分總自動更新一次
        /// </summary>
        public int TimerDelay { get; set; } = 30 * 60 * 1000;

        /// <summary>
        /// 金焰的世界
        /// 2018-11-14
        /// Redis連接字元串
        /// </summary>
        public string RedisConnectionString { get; set; }

        /// <summary>
        /// 金焰的世界
        /// 2019-03-03
        /// 配置哨兵或分區時使用
        /// </summary>
        public string[] RedisSentinelOrPartitionConStr { get; set; }

        /// <summary>
        /// 金焰的世界
        /// 2019-03-03
        /// Redis部署方式,預設使用普通方式
        /// </summary>
        public RedisStoreMode RedisStoreMode { get; set; } = RedisStoreMode.Normal;

        /// <summary>
        /// 金焰的計界
        /// 2019-03-03
        /// 做集群緩存同步時使用,會訂閱所有正則匹配的事件
        /// </summary>
        public string RedisOcelotKeyPrefix { get; set; } = "CzarOcelot";

        /// <summary>
        /// 金焰的世界
        /// 2019-03-03
        /// 是否啟用集群環境,如果非集群環境直接本地緩存+資料庫即可
        /// </summary>
        public bool ClusterEnvironment { get; set; } = false;

        /// <summary>
        /// 金焰的世界
        /// 2018-11-15
        /// 是否啟用客戶端授權,預設不開啟
        /// </summary>
        public bool ClientAuthorization { get; set; } = false;

        /// <summary>
        /// 金焰的世界
        /// 2018-11-15
        /// 伺服器緩存時間,預設30分鐘
        /// </summary>
        public int CzarCacheTime { get; set; } = 1800;
        /// <summary>
        /// 金焰的世界
        /// 2018-11-15
        /// 客戶端標識,預設 client_id
        /// </summary>
        public string ClientKey { get; set; } = "client_id";

        /// <summary>
        /// 金焰的世界
        /// 2018-11-18
        /// 是否開啟自定義限流,預設不開啟
        /// </summary>
        public bool ClientRateLimit { get; set; } = false;
    }
}

在配置文件中修改了redis相關配置,支持使用redis的普通模式、集群模式、哨兵模式、分區模式,配置方式可參考csrediscore開源項目。

然後修改ServiceCollectionExtensions.cs代碼,註入相關實現和redis客戶端。

            builder.Services.AddMemoryCache(); //添加本地緩存
            #region 啟動Redis緩存,並支持普通模式 官方集群模式  哨兵模式 分區模式
            if (options.ClusterEnvironment)
            {
                //預設使用普通模式
                var csredis = new CSRedis.CSRedisClient(options.RedisConnectionString);
                switch (options.RedisStoreMode)
                {
                    case RedisStoreMode.Partition:
                        var NodesIndex = options.RedisSentinelOrPartitionConStr;
                        Func<string, string> nodeRule = null;
                        csredis = new CSRedis.CSRedisClient(nodeRule, options.RedisSentinelOrPartitionConStr);
                        break;
                    case RedisStoreMode.Sentinel:
                        csredis = new CSRedis.CSRedisClient(options.RedisConnectionString, options.RedisSentinelOrPartitionConStr);
                        break;
                }
                //初始化 RedisHelper
                RedisHelper.Initialization(csredis);
            }
            #endregion
            builder.Services.AddSingleton<IOcelotCache<FileConfiguration>, CzarMemoryCache<FileConfiguration>>();
            builder.Services.AddSingleton<IOcelotCache<InternalConfiguration>, CzarMemoryCache<InternalConfiguration>>();
            builder.Services.AddSingleton<IOcelotCache<CachedResponse>, CzarMemoryCache<CachedResponse>>();
            builder.Services.AddSingleton<IInternalConfigurationRepository, RedisInternalConfigurationRepository>();
            builder.Services.AddSingleton<IOcelotCache<ClientRoleModel>, CzarMemoryCache<ClientRoleModel>>();
            builder.Services.AddSingleton<IOcelotCache<RateLimitRuleModel>, CzarMemoryCache<RateLimitRuleModel>>();
            builder.Services.AddSingleton<IOcelotCache<RemoteInvokeMessage>, CzarMemoryCache<RemoteInvokeMessage>>();
            builder.Services.AddSingleton<IOcelotCache<CzarClientRateLimitCounter?>, CzarMemoryCache<CzarClientRateLimitCounter?>>();

現在需要實現redis訂閱來更新本地的緩存信息,在項目啟動時判斷是否開啟集群模式,如果開啟就啟動訂閱,實現代碼如下:

public static async Task<IApplicationBuilder> UseCzarOcelot(this IApplicationBuilder builder, OcelotPipelineConfiguration pipelineConfiguration)
{
    //重寫創建配置方法
    var configuration = await CreateConfiguration(builder);
    ConfigureDiagnosticListener(builder);
    CacheChangeListener(builder);
    return CreateOcelotPipeline(builder, pipelineConfiguration);
}
/// <summary>
/// 金焰的世界
/// 2019-03-03
/// 添加緩存數據變更訂閱
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
private static void CacheChangeListener(IApplicationBuilder builder)
{
    var config= builder.ApplicationServices.GetService<CzarOcelotConfiguration>();
    var _cache= builder.ApplicationServices.GetService<IMemoryCache>();
    if (config.ClusterEnvironment)
    {
        //訂閱滿足條件的所有事件
        RedisHelper.PSubscribe(new[] { config.RedisOcelotKeyPrefix + "*" }, message =>
              {
                  var key = message.Channel;
                  _cache.Remove(key); //直接移除,如果有請求從redis里取
                  //或者直接判斷本地緩存是否存在,如果存在更新,可自行實現。
              });
    }
}

使用的是從配置文件提取的正則匹配的所有KEY都進行訂閱,由於本地緩存增加了定時過期策略,所以為了實現方便,當發現redis數據發生變化,所有訂閱端直接移除本地緩存即可,如果有新的請求直接從redis取,然後再次緩存,防止集群客戶端緩存信息不一致。

為了區分不同的緩存實體,便於在原始數據發送變更時進行更新,定義CzarCacheRegion類。

namespace Czar.Gateway.Configuration
{
    /// <summary>
    /// 緩存所屬區域
    /// </summary>
    public class CzarCacheRegion
    {
        /// <summary>
        /// 授權
        /// </summary>
        public const string AuthenticationRegion = "CacheClientAuthentication";

        /// <summary>
        /// 路由配置
        /// </summary>
        public const string FileConfigurationRegion = "CacheFileConfiguration";

        /// <summary>
        /// 內部配置
        /// </summary>
        public const string InternalConfigurationRegion = "CacheInternalConfiguration";

        /// <summary>
        /// 客戶端許可權
        /// </summary>
        public const string ClientRoleModelRegion = "CacheClientRoleModel";

        /// <summary>
        /// 限流規則
        /// </summary>
        public const string RateLimitRuleModelRegion = "CacheRateLimitRuleModel";

        /// <summary>
        /// Rpc遠程調用
        /// </summary>
        public const string RemoteInvokeMessageRegion = "CacheRemoteInvokeMessage";

        /// <summary>
        /// 客戶端限流
        /// </summary>
        public const string CzarClientRateLimitCounterRegion = "CacheCzarClientRateLimitCounter";
    }
}

現在只需要修改緩存的region為定義的值即可,唯一需要改動的代碼就是把之前寫死的代碼改成如下代碼即可。

var enablePrefix = CzarCacheRegion.AuthenticationRegion;

3.開發緩存變更介面

現在整個二級緩存基本完成,但是還遇到一個問題就是外部如何根據資料庫變更數據時來修改緩存數據,這時就需要提供外部修改api來實現。

添加CzarCacheController.cs對外部提供緩存更新相關介面,詳細代碼如下:

using Czar.Gateway.Authentication;
using Czar.Gateway.Configuration;
using Czar.Gateway.RateLimit;
using Czar.Gateway.Rpc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Ocelot.Configuration;
using Ocelot.Configuration.Creator;
using Ocelot.Configuration.Repository;
using System;
using System.Threading.Tasks;

namespace Czar.Gateway.Cache
{
    /// <summary>
    /// 提供外部緩存處理介面
    /// </summary>
    [Authorize]
    [Route("CzarCache")]
    public class CzarCacheController : Controller
    {
        private readonly CzarOcelotConfiguration _options;
        private readonly IClientAuthenticationRepository _clientAuthenticationRepository;
        private IFileConfigurationRepository _fileConfigurationRepository;
        private IInternalConfigurationCreator _internalConfigurationCreator;
        private readonly IClientRateLimitRepository _clientRateLimitRepository;
        private readonly IRpcRepository _rpcRepository;
        private readonly IMemoryCache _cache;
        public CzarCacheController(IClientAuthenticationRepository clientAuthenticationRepository, CzarOcelotConfiguration options,
          IFileConfigurationRepository fileConfigurationRepository,
          IInternalConfigurationCreator internalConfigurationCreator,
          IClientRateLimitRepository clientRateLimitRepository,
          IRpcRepository rpcRepository,
          IMemoryCache cache)
        {
            _clientAuthenticationRepository = clientAuthenticationRepository;
            _options = options;
            _fileConfigurationRepository = fileConfigurationRepository;
            _internalConfigurationCreator = internalConfigurationCreator;
            _clientRateLimitRepository = clientRateLimitRepository;
            _rpcRepository = rpcRepository;
            _cache = cache;
        }

        /// <summary>
        /// 更新客戶端地址訪問授權介面
        /// </summary>
        /// <param name="clientid">客戶端ID</param>
        /// <param name="path">請求模板</param>
        /// <returns></returns>
        [HttpPost]
        [Route("ClientRule")]
        public async Task UpdateClientRuleCache(string clientid, string path)
        {
            var region = CzarCacheRegion.AuthenticationRegion;
            var key = CzarOcelotHelper.ComputeCounterKey(region, clientid, "", path);
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientAuthenticationRepository.ClientAuthenticationAsync(clientid, path);
            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis緩存
                RedisHelper.Publish(key, data.ToJson()); //發佈事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新網關配置路由信息
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        [Route("InternalConfiguration")]
        public async Task UpdateInternalConfigurationCache()
        {
            var key = CzarCacheRegion.InternalConfigurationRegion;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, "", key);
            var fileconfig = await _fileConfigurationRepository.Get();
            var internalConfig = await _internalConfigurationCreator.Create(fileconfig.Data);
            var config = (InternalConfiguration)internalConfig.Data;
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, config); //加入redis緩存
                RedisHelper.Publish(key, config.ToJson()); //發佈事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 刪除路由配合的緩存信息
        /// </summary>
        /// <param name="region">區域</param>
        /// <param name="downurl">下端路由</param>
        /// <returns></returns>
        [HttpPost]
        [Route("Response")]
        public async Task DeleteResponseCache(string region,string downurl)
        {
            var key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, downurl);
            if (_options.ClusterEnvironment)
            {
                await RedisHelper.DelAsync(key);
                RedisHelper.Publish(key, "");//發佈時間
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新客戶端限流規則緩存
        /// </summary>
        /// <param name="clientid">客戶端ID</param>
        /// <param name="path">路由模板</param>
        /// <returns></returns>
        [HttpPost]
        [Route("RateLimitRule")]
        public async Task UpdateRateLimitRuleCache(string clientid, string path)
        {
            var region = CzarCacheRegion.RateLimitRuleModelRegion;
            var key = clientid + path;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientRateLimitRepository.CheckClientRateLimitAsync(clientid, path);
            var data = new RateLimitRuleModel() { RateLimit = result.RateLimit, rateLimitOptions = result.rateLimitOptions };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis緩存
                RedisHelper.Publish(key, data.ToJson()); //發佈事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新客戶端是否開啟限流緩存
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("ClientRole")]
        public async Task UpdateClientRoleCache(string path)
        {
            var region = CzarCacheRegion.ClientRoleModelRegion;
            var key = path;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientRateLimitRepository.CheckReRouteRuleAsync(path);
            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis緩存
                RedisHelper.Publish(key, data.ToJson()); //發佈事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        /// <summary>
        /// 更新呢客戶端路由白名單緩存
        /// </summary>
        /// <param name="clientid"></param>
        /// <param name="path"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("ClientReRouteWhiteList")]
        public async Task UpdateClientReRouteWhiteListCache(string clientid, string path)
        {
            var region = CzarCacheRegion.ClientReRouteWhiteListRegion;
            var key = clientid + path;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _clientRateLimitRepository.CheckClientReRouteWhiteListAsync(clientid, path);
            var data = new ClientRoleModel() { CacheTime = DateTime.Now, Role = result };
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, data); //加入redis緩存
                RedisHelper.Publish(key, data.ToJson()); //發佈事件
            }
            else
            {
                _cache.Remove(key);
            }
        }

        [HttpPost]
        [Route("Rpc")]
        public async Task UpdateRpcCache(string UpUrl)
        {
            var region = CzarCacheRegion.RemoteInvokeMessageRegion;
            var key = UpUrl;
            key = CzarOcelotHelper.GetKey(_options.RedisOcelotKeyPrefix, region, key);
            var result = await _rpcRepository.GetRemoteMethodAsync(UpUrl);
            if (_options.ClusterEnvironment)
            {
                RedisHelper.Set(key, result); //加入redis緩存
                RedisHelper.Publish(key, result.ToJson()); //發佈事件
            }
            else
            {
                _cache.Remove(key);
            }
        }
    }
}

現在基本實現整個緩存的更新策略,只要配合後臺管理界面,在相關緩存原始數據發送變更時,調用對應介面即可完成redis緩存的更新,並自動通知集群的所有本機清理緩存等待重新獲取。

介面的調用方式參考之前我寫的配置信息介面變更那篇即可。

四、性能測試

完成了改造後,我們拿改造前網關、改造後網關、原始Ocelot、直接調用API四個環境分別測試性能指標,由於測試環境有效,我直接使用本機環境,然後是Apache ab測試工具測試下相關性能(本測試不一定准確,只作為參考指標),測試的方式是使用100個併發請求10000次,測試結果分別如下。

1、改造前網關性能

2、改造後網關性能

3、Ocelot預設網關性能

4、直接調用API性能

本測試僅供參考,因為由於網關和服務端都在本機環境部署,所以使用網關和不使用網關性能差別非常小,如果分開部署可能性別差別會明顯寫,這不是本篇討論的重點。

從測試中可以看到,重構的網關改造前和改造後性能有2倍多的提升,且與原生的Ocelot性能非常接近。

五、總結

本篇主要講解瞭如何使用redis的發佈訂閱來實現二級緩存功能,並提供了緩存的更新相關介面供外部程式調用,避免出現集群環境下無法更新緩存數據導致提取數據不一致情況,但是針對每個客戶端獨立限流這塊集群環境目前還是採用的redis的方式未使用本地緩存,如果有寫的不對或有更好方式的,也希望多提寶貴意見。

本篇相關源碼地址:https://github.com/jinyancao/czar.gateway


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

-Advertisement-
Play Games
更多相關文章
  • 配置,幾乎所有的應用程式都離不開它。.Net Framework時代我們使用App.config、Web.config,到了.Net Core的時代我們使用appsettings.json,這些我們再熟悉不過了。然而到了容器化、微服務的時代,這些本地文件配置有的時候就不太合適了。當你把本地部署的服務 ...
  • 所有的類型都從 System.Object 派生 1、類型System.Object 運行時要求每一個類型都是從System.Object派生,如果沒有顯示的寫明繼承關係,最後都會預設的從System.Object來派生。System.Object提供了四個公用方法和兩個收保護方法: 2、new 操 ...
  • c#方法的重載:分為實例方法重載和靜態方法重載倆種 1.實例方法重載的調用特點 首先寫三個Add方法和三個Sub方法 特點:編譯器將自動更具方法的參數個數和類型自動匹配類的對應方法。 2.實例方法重載的好處 1.減少類的對外介面(只顯示一個方法),降低類的複雜度。 2.便於用戶使用(相同功能的方法名 ...
  • 想寫博客不知道從何處開始,就從回憶開始吧. 第一個就從自定義日曆控制項開始 產生背景: 大概2015年時候有個項目要用到日曆,用預設日曆展示給用戶看,用戶毫不客氣都說界面太醜,最好做成像百度日曆那樣方便使用。 花費了一些時間感覺模仿相似度達到95%,模糊不清楚是因為圖片被壓縮了,瀏覽器中看圖片還是挺正... ...
  • 在項目實踐中,我們 可能會遇到需要將一些控制項上顯示的內容只顯示一段時間過後清空。 下麵我們來實現這種操作: 首先需要註意的是:在wpf中涉及到界面操作的話,一定要使用定時器DispatcherTime,DispatcherTimer是為wpf專門設計的,不然的話使用其他種類的定時器會提示界面資源被其 ...
  • 之前做過一個MES系統,發一些裡面的截圖。如果有朋友也用這個框架。或者有興趣可以一起學習學習。使用開發工具VS2013,資料庫SqlServer2008和Oracle11C。插件dev15.2,開發模式基於MVC三層模式。部分截圖。 資料庫連接工具,保存到配置文件。 ORM,BLL,DAL等生成工具 ...
  • 一 在negut添加EPPlus.dll庫文件。 之前有寫過直接只用Microsoft.Office.Interop.Excel 導出EXCEL,並生成Chart表,非常耗時,所以找了個EPPlus控制項。 二 代碼部分 三 效果 ...
  • 之前看公司web前端做了個 圓形的水波紋 進度條,就想用wpf 做一個,奈何自己太菜 一直做不出來,在看過 “普通的地球人” 的 “ WPF實現三星手機充電界面 博客之後 我也來照葫蘆畫個瓢。 廢話不多說 先貼一下效果圖 雖然樣子 low 了些 但是基本滿足我的需求了,下麵是代碼 前端 後臺 美中不 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...