標籤: 網頁設計

  • Dart Memo for Android Developers

    Dart Memo for Android Developers

    Dart語言一些語法特點和編程規範.

    本文適合: 日常使用Kotlin, 突然想寫個Flutter程序的Android程序員.

    Dart語言

    完整的請看A tour of the Dart language

    • 創建對象可以不用new. -> 並且規範不讓用new, lint會報錯.
    • 聲明變量可以用var, 也可以用具體類型如String. 不變量用final, 常量用const.
    • 沒有訪問修飾符, 用_來表示私有: 文件級別.
    • 字符串可以用單引號'.
    • 語句結尾要用;.
    • 創建數組可以用: var list = [1, 2, 3];.
    • assert()常用來斷定開發時不可能會出現的情況.
    • 空測試操作符: ??.
    • 過濾操作符: where.
    • 兩個點..表示鏈式調用.
    • dynamic說明類型未指定.
    • 除了throw異常, 還可以throw別的東西, 比如字符串.

    函數

    • 函數返回值在函數最開頭, 可以不標. -> 但是規範會建議標註返回值.
    bool isNoble(int atomicNumber) {
      return _nobleGases[atomicNumber] != null;
    }
    
    • =>箭頭符號, 用來簡化一句話的方法.
    bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
    

    構造函數

    • 構造函數{}表示帶名字, 參數可選, 若要必選加上@required.
    const Scrollbar({Key key, @required Widget child})
    
    • 構造函數名可以是ClassName或者ClassName.identifier.
    • 空構造函數體可以省略, 用;結尾就行:
    class Point {
      double x, y;
      Point(this.x, this.y);
    }
    

    這裡會初始化相應的變量, 也不用聲明具體的參數類型.

    • factory構造, 可以用來返回緩存實例, 或者返回類型的子類:
    factory Logger(String name) {
        return _cache.putIfAbsent(name, () => Logger._internal(name));
    }
    

    異步代碼

    Future<String> lookUpVersion() async => '1.0.0';
    
    Future checkVersion() async {
      var version = await lookUpVersion();
      // Do something with version
    }
    

    編程規範類

    完整的規範在這裏: Effective Dart.

    有一些Good和Bad的舉例, 這裏僅列出比較常用的幾項.

    • 文件名要蛇形命名: lowercase_with_underscores. 類名: UpperCamelCase.
    • 對自己程序的文件, 兩種import都可以(package開頭或者相對路徑), 但是要保持一致.
    • Flutter程序嵌套比較多, 要用結尾的,來幫助格式化.

    本文緣由

    年初的時候學了一陣子Flutter, 寫了各種大小demo. 結果隔了兩個月之後, 突然心血來潮想寫個小東西, 打開Android Studio, 首先發現創建Flutter程序的按鈕都不見了. (估計是Android Studio4.0升級之後Flutter的插件沒跟上).

    接着用命令行創建了工程, 打開之後稍微整理了一下心情, 然後就….懵逼了.

    突然不知道如何下手.
    宏觀的東西還記得, 要用什麼package, 基本常用的幾個Widget都是啥, 但是微觀的, 忘了函數和數組都是咋定義的了.
    這種懵逼的狀態令我很憤怒, 果然是上年紀了嗎, 無縫切換個語言都不行.

    於是就想着還是寫個備忘錄吧.

    References

    • A tour of the Dart language
    • Effective Dart

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • 【asp.net core 系列】9 實戰之 UnitOfWork以及自定義代碼生成

    【asp.net core 系列】9 實戰之 UnitOfWork以及自定義代碼生成

    0. 前言

    在前一篇中我們創建了一個基於EF的數據查詢接口實現基類,這一篇我將帶領大家講一下為這EF補充一些功能,並且提供一個解決避免寫大量配置類的方案。

    1. SaveChanges的外移

    在之前介紹EF Core的時候,我們提到過使用EF需要在每次使用之後,調用一次SaveChanges將數據提交給數據庫。在實際開發中,我們不能添加一條數據或者做一次修改就調用一次SaveChanges,這完全不現實。因為每次調用SaveChanges是EF向數據庫提交變更的時候,所以EF推薦的是每次執行完用戶的請求之後統一提交數據給數據庫。

    這樣就會造成一個問題,可能也不是問題:我們需要一個接口來管理EF 的SaveChanges操作。

    1.1 創建一個IUnitOfWork接口

    通常我們會在Domain項目中添加一個IUnitOfWork接口,這個接口有一個方法就是SaveChanges,代碼如下:

    namespace Domain.Insfrastructure
    {
        public interface IUnitOfWork
        {
            void SaveChanges();
        }
    }
    

    這個方法的意思表示到執行該方法的時候,一個完整的工作流程執行完成了。也就是說,當執行該方法后,當前請求不會再與數據庫發生連接。

    1.2 實現IUnitOfWork接口

    在 Domain.Implement中添加IUnitOfWork實現類:

    using Domain.Insfrastructure;
    using Microsoft.EntityFrameworkCore;
    
    namespace Domain.Implements.Insfrastructure
    {
        public class UnitOfWork: IUnitOfWork
        {
            private DbContext DbContext;
            public UnitOfWork(DbContext context)
            {
                DbContext = context;
            }
    
            public void SaveChanges()
            {
                DbContext.SaveChanges();
            }
        }
    }
    

    1.3 調用時機

    到現在我們已經創建了一個UnitOfWork的方法,那麼問題來了,我們該在什麼時候調用呢,或者說如何調用呢?

    我的建議是創建一個ActionFilter,針對所有的控制器進行SaveChanges進行處理。當然了,也可以在控制器中持有一個IUnitOfWork的示例,然後在Action結束的時候,執行SaveChanges。不過這樣存在一個問題,可能會存在遺漏的方法。所以我推薦這樣操作,這裏簡單演示一下如何創建攔截器:

    在Web的根目錄下,創建一個Filters目錄,這個目錄里用來存儲一些過濾器,創建我們需要的過濾器:

    using Domain.Insfrastructure;
    using Microsoft.AspNetCore.Mvc.Filters;
    
    namespace Web.Filters
    {
        public class UnitOfWorkFilterAttribute : ActionFilterAttribute
        {
            public IUnitOfWork UnitOfWork;
    
            public override void OnActionExecuted(ActionExecutedContext context)
            {
                UnitOfWork.SaveChanges();
            }
        }
    }
    

    使用一個ActionFilter可以很方便的解決一些容易遺漏但又必須執行的代碼。這裏就先不介紹如何配置Filter的啟用和詳細介紹了,請允許我賣個關子。當然了,有些小夥伴肯定也能猜到這是一個Attribute類,所以可以按照Attribute給Controller打標記。

    2. 創建一個簡單的代碼生成方法

    之前在介紹EF的時候,有個小夥伴跟我說,還要寫配置文件啊,太麻煩了。是的,之前我介紹了很多關於寫配置文件不使用特性的好處,但不解決這個問題就無法真正體檢配置類的好處。

    雖然說,EF Core約定優先,但是如果默認約定的話,得在DBContext中聲明 DbSet<T> 來聲明這個字段,實體類少的話,比較簡單。如果多個數據表的話,就會非常麻煩。

    所以這時候就要使用工具類, 那麼簡單的分析一下,這個工具類需要有哪些功能:

    • 第一步,找到實體類並解析出實體類的類名
    • 第二步,生成配置文件
    • 第三步,創建對應的Repository接口和實現類

    很簡單的三步,但是難點就是找實體類並解析出實體類名。

    在Util項目中添加一個Develop目錄,並創建Develop類:

    namespace Utils.Develop
    {
        public class Develop
        {
            
        }
    }
    

    定位當前類所在目錄,通過

    Directory.GetCurrentDirectory()
    

    這個方法可以獲取當前執行的DLL所在目錄,當然不同的編譯器在執行的時候,會有微妙的不同。所以我們需要以此為根據然後獲取項目的根目錄,一個簡單的方法,查找*.sln 所在目錄:

    public static string CurrentDirect
    {
        get
        {
            var execute = Directory.GetCurrentDirectory();
            var parent = Directory.GetParent(execute);
            while(parent.GetFiles("*.sln",SearchOption.TopDirectoryOnly).Length == 0)
            {
                parent = parent.Parent;
                if(parent == null)
                {
                    return null;
                }
            }
            return parent.FullName;
        }
    }
    

    2.1 獲取實體類

    那麼獲取到根目錄之後,我們下一步就是獲取實體類。因為我們的實體類都要求是繼承BaseEntity或者命名空間都是位於Data.Models下面。當然這個名稱都是根據實際業務場景約束的,這裏只是以當前項目舉例。那麼,我們可以通過以下方法找到我們設置的實體類:

    public static Type[] LoadEntities()
    {
        var assembly = Assembly.Load("Data");
        var allTypes = assembly.GetTypes();
        var ofNamespace = allTypes.Where(t => t.Namespace == "Data.Models" || t.Namespace.StartsWith("Data.Models."));
        var subTypes = allTypes.Where(t => t.BaseType.Name == "BaseEntity`1");
        return ofNamespace.Union(subTypes).ToArray();
    }
    

    通過 Assembly加載Data的程序集,然後選擇出符合我們要求的實體類。

    2.2 編寫Repository接口

    我們先約定Model的Repository接口定義在 Domain/Repository目錄下,所以它們的命名空間應該是:

    namespace Domain.Repository	
    {
    }
    

    假設目錄情況與Data/Models下面的代碼結構保持一致,然後生成代碼應該如下:

    public static void CreateRepositoryInterface(Type type)
    {
        var targetNamespace = type.Namespace.Replace("Data.Models", "");
        if (targetNamespace.StartsWith("."))
        {
            targetNamespace = targetNamespace.Remove(0);
        }
        var targetDir = Path.Combine(new[]{CurrentDirect,"Domain", "Repository"}.Concat(
            targetNamespace.Split('.')).ToArray());
        if (!Directory.Exists(targetDir))
        {
            Directory.CreateDirectory(targetDir);
        }
    
        var baseName = type.Name.Replace("Entity","");
    
        if (!string.IsNullOrEmpty(targetNamespace))
        {
            targetNamespace = $".{targetNamespace}";
        }
        var file = $"using {type.Namespace};\r\n"
            + $"using Domain.Insfrastructure;\r\n"
            + $"namespace Domain.Repository{targetNamespace}\r\n"
            + "{\r\n"
            + $"\tpublic interface I{baseName}ModifyRepository : IModifyRepository<{type.Name}>\r\n" +
            "\t{\r\n\t}\r\n"
            + $"\tpublic interface I{baseName}SearchRepository : ISearchRepository<{type.Name}>\r\n" +
            "\t{\r\n\t}\r\n}";
    
        File.WriteAllText(Path.Combine(targetDir, $"{baseName}Repository.cs"), file);
    }
    
    

    2.3 編寫Repository的實現類

    因為我們提供了一個基類,所以我們在生成方法的時候,推薦繼承這個類,那麼實現方法應該如下:

    public static void CreateRepositoryImplement(Type type)
    {
        var targetNamespace = type.Namespace.Replace("Data.Models", "");
        if (targetNamespace.StartsWith("."))
        {
            targetNamespace = targetNamespace.Remove(0);
        }
    
        var targetDir = Path.Combine(new[] {CurrentDirect, "Domain.Implements", "Repository"}.Concat(
            targetNamespace.Split('.')).ToArray());
        if (!Directory.Exists(targetDir))
        {
            Directory.CreateDirectory(targetDir);
        }
        var baseName = type.Name.Replace("Entity", "");
        if (!string.IsNullOrEmpty(targetNamespace))
        {
            targetNamespace = $".{targetNamespace}";
        }
    
        var file = $"using {type.Namespace};" +
            $"\r\nusing Domain.Implements.Insfrastructure;" +
            $"\r\nusing Domain.Repository{targetNamespace};" +
            $"\r\nusing Microsoft.EntityFrameworkCore;" +
            $"namespace Domain.Implements.Repository{targetNamespace}\r\n" +
            "{" +
            $"\r\n\tpublic class {baseName}Repository :BaseRepository<{type.Name}> ,I{baseName}ModifyRepository,I{baseName}SearchRepository " +
            "\r\n\t{" +
            $"\r\n\t\tpublic {baseName}Repository(DbContext context) : base(context)"+
            "\r\n\t\t{"+
            "\r\n\t\t}\r\n"+
            "\t}\r\n}";
        File.WriteAllText(Path.Combine(targetDir, $"{baseName}Repository.cs"), file);
    }
    

    2.4 配置文件的生成

    仔細觀察一下代碼,可以發現整體都是十分簡單的。所以這篇就不掩飾如何生成配置文件了,小夥伴們可以自行嘗試一下哦。具體實現可以等一下篇哦。

    3. 總結

    這一篇初略的介紹了兩個用來輔助EF Core實現的方法或類,這在開發中很重要。UnitOfWork用來確保一次請求一個工作流程,簡單的代碼生成類讓我們能讓我們忽略那些繁重的創建同類代碼的工作。

    更多內容煩請關注我的博客《高先生小屋》

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※Google地圖已可更新顯示潭子電動車充電站設置地點!!

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計最專業,超強功能平台可客製化

  • 曹工說Redis源碼(8)–面試時,redis 內存淘汰總被問,但是總答不好

    曹工說Redis源碼(8)–面試時,redis 內存淘汰總被問,但是總答不好

    文章導航

    Redis源碼系列的初衷,是幫助我們更好地理解Redis,更懂Redis,而怎麼才能懂,光看是不夠的,建議跟着下面的這一篇,把環境搭建起來,後續可以自己閱讀源碼,或者跟着我這邊一起閱讀。由於我用c也是好幾年以前了,些許錯誤在所難免,希望讀者能不吝指出。

    曹工說Redis源碼(1)– redis debug環境搭建,使用clion,達到和調試java一樣的效果

    曹工說Redis源碼(2)– redis server 啟動過程解析及簡單c語言基礎知識補充

    曹工說Redis源碼(3)– redis server 啟動過程完整解析(中)

    曹工說Redis源碼(4)– 通過redis server源碼來理解 listen 函數中的 backlog 參數

    曹工說Redis源碼(5)– redis server 啟動過程解析,以及EventLoop每次處理事件前的前置工作解析(下)

    曹工說Redis源碼(6)– redis server 主循環大體流程解析

    曹工說Redis源碼(7)– redis server 的周期執行任務,到底要做些啥

    什麼是內存淘汰

    內存淘汰,和平時我們設置redis key的過期時間,不是一回事;內存淘汰是說,假設我們限定redis只能使用8g內存,現在已經使用了這麼多了(包括設置了過期時間的key和沒設過期時間的key),那,後續的set操作,還怎麼辦呢?

    是不是只能報錯了?

    那不行啊,不科學吧,因為有的key,可能已經很久沒人用了,可能以後也不會再用到了,那我們是不是可以把這類key給幹掉呢?

    幹掉key的過程,就是內存淘汰。

    內存淘汰什麼時候啟用

    當我們在配置文件里設置了如下屬性時:

    # maxmemory <bytes>
    

    默認,該屬性是被註釋掉的。

    其實,這個配置項的註釋,相當有價值,我們來看看:

    # Don't use more memory than the specified amount of bytes.
    # When the memory limit is reached Redis will try to remove keys
    # according to the eviction policy selected (see maxmemory-policy).
    #
    # If Redis can't remove keys according to the policy, or if the policy is
    # set to 'noeviction', Redis will start to reply with errors to commands
    # that would use more memory, like SET, LPUSH, and so on, and will continue
    # to reply to read-only commands like GET.
    #
    # This option is usually useful when using Redis as an LRU cache, or to set
    # a hard memory limit for an instance (using the 'noeviction' policy).
    #
    # WARNING: If you have slaves attached to an instance with maxmemory on,
    # the size of the output buffers needed to feed the slaves are subtracted
    # from the used memory count, so that network problems / resyncs will
    # not trigger a loop where keys are evicted, and in turn the output
    # buffer of slaves is full with DELs of keys evicted triggering the deletion
    # of more keys, and so forth until the database is completely emptied.
    #
    # In short... if you have slaves attached it is suggested that you set a lower
    # limit for maxmemory so that there is some free RAM on the system for slave
    # output buffers (but this is not needed if the policy is 'noeviction').
    #
    # maxmemory <bytes>
    

    渣翻譯如下:

    不能使用超過指定數量bytes的內存。當該內存限制被達到時,redis會根據過期策略(eviction policy,通過參數 maxmemory-policy來指定)來驅逐key。

    如果redis根據指定的策略,或者策略被設置為“noeviction”,redis會開始針對如下這種命令,回復錯誤。什麼命令呢?會使用更多內存的那類命令,比如set、lpush;只讀命令還是不受影響,可以正常響應。

    該選項通常在redis使用LRU緩存時有用,或者在使用noeviction策略時,設置一個進程級別的內存limit。

    內存淘汰策略

    所謂策略,意思是,當我們要刪除部分key的時候,刪哪些,不刪哪些?是不是需要一個策略?比如是隨機刪,就像滅霸一樣?還是按照lru時間來刪,lru的策略意思就是,最近最少使用的key,將被優先刪除。

    總之,我們需要定一個規則。

    redis默認支持以下策略:

    # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
    # is reached. You can select among five behaviors:
    # 
    # volatile-lru -> remove the key with an expire set using an LRU algorithm
    # allkeys-lru -> remove any key accordingly to the LRU algorithm
    # volatile-random -> remove a random key with an expire set
    # allkeys-random -> remove a random key, any key
    # volatile-ttl -> remove the key with the nearest expire time (minor TTL)
    # noeviction -> don't expire at all, just return an error on write operations
    # 
    # Note: with any of the above policies, Redis will return an error on write
    #       operations, when there are not suitable keys for eviction.
    #
    #       At the date of writing this commands are: set setnx setex append
    #       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
    #       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
    #       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
    #       getset mset msetnx exec sort
    #
    # The default is:
    #
    # maxmemory-policy noeviction
    maxmemory-policy allkeys-lru
    
    針對設置了過期時間的,使用lru算法
    # volatile-lru -> remove the key with an expire set using an LRU algorithm
    
    針對全部key,使用lru算法
    # allkeys-lru -> remove any key accordingly to the LRU algorithm
    
    針對設置了過期時間的,隨機刪
    # volatile-random -> remove a random key with an expire set
    
    針對全部key,隨機刪
    # allkeys-random -> remove a random key, any key
    
    針對設置了過期時間的,馬上要過期的,刪掉
    # volatile-ttl -> remove the key with the nearest expire time (minor TTL)
    
    不過期,不能寫了,就報錯
    # noeviction -> don't expire at all, just return an error on write operations
    

    一般呢,我們會設置為:

    allkeys-lru,即,針對全部key,進行lru。

    源碼實現

    配置讀取

    在如下結構體中,定義了如下字段:

    struct redisServer {
    	...
    	unsigned long long maxmemory;   /* Max number of memory bytes to use */
        int maxmemory_policy;           /* Policy for key eviction */
        int maxmemory_samples;          /* Pricision of random sampling */
        ...
    }
    

    當我們在配置文件中,進入如下配置時,該結構體中幾個字段的值如下:

    maxmemory 3mb
    maxmemory-policy allkeys-lru
    # maxmemory-samples 5  這個取了默認值
    

    maxmemory_policy為3,是因為枚舉值為3:

    #define REDIS_MAXMEMORY_VOLATILE_LRU 0
    #define REDIS_MAXMEMORY_VOLATILE_TTL 1
    #define REDIS_MAXMEMORY_VOLATILE_RANDOM 2
    #define REDIS_MAXMEMORY_ALLKEYS_LRU 3
    #define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4
    #define REDIS_MAXMEMORY_NO_EVICTION 5
    #define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION
    

    處理命令時,判斷是否進行內存淘汰

    在處理命令的時候,會調用中的

    redis.c  processCommand
        
    int processCommand(redisClient *c) {
        /* The QUIT command is handled separately. Normal command procs will
         * go through checking for replication and QUIT will cause trouble
         * when FORCE_REPLICATION is enabled and would be implemented in
         * a regular command proc. */
        // 特別處理 quit 命令
        void *commandName = c->argv[0]->ptr;
        redisLog(REDIS_NOTICE, "The server is now processing %s", commandName);
    
        if (!strcasecmp(c->argv[0]->ptr, "quit")) {
            addReply(c, shared.ok);
            c->flags |= REDIS_CLOSE_AFTER_REPLY;
            return REDIS_ERR;
        }
    
        /* Now lookup the command and check ASAP about trivial error conditions
         * such as wrong arity, bad command name and so forth. */
        // 1 查找命令,並進行命令合法性檢查,以及命令參數個數檢查
        c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
        if (!c->cmd) {
            // 沒找到指定的命令
            flagTransaction(c);
            addReplyErrorFormat(c, "unknown command '%s'",
                                (char *) c->argv[0]->ptr);
            return REDIS_OK;
        }
    
        /* Check if the user is authenticated */
        //2 檢查認證信息
        if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand) {
            flagTransaction(c);
            addReply(c, shared.noautherr);
            return REDIS_OK;
        }
    
        /* If cluster is enabled perform the cluster redirection here.
         *
         * 3 如果開啟了集群模式,那麼在這裏進行轉向操作。
         *
         * However we don't perform the redirection if:
         *
         * 不過,如果有以下情況出現,那麼節點不進行轉向:
         *
         * 1) The sender of this command is our master.
         *    命令的發送者是本節點的主節點
         *
         * 2) The command has no key arguments. 
         *    命令沒有 key 參數
         */
        if (server.cluster_enabled &&
            !(c->flags & REDIS_MASTER) &&
            !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0)) {
            int hashslot;
    
            // 集群已下線
            if (server.cluster->state != REDIS_CLUSTER_OK) {
                flagTransaction(c);
                addReplySds(c, sdsnew("-CLUSTERDOWN The cluster is down. Use CLUSTER INFO for more information\r\n"));
                return REDIS_OK;
    
                // 集群運作正常
            } else {
                int error_code;
                clusterNode *n = getNodeByQuery(c, c->cmd, c->argv, c->argc, &hashslot, &error_code);
                // 不能執行多鍵處理命令
                if (n == NULL) {
                    flagTransaction(c);
                    if (error_code == REDIS_CLUSTER_REDIR_CROSS_SLOT) {
                        addReplySds(c, sdsnew("-CROSSSLOT Keys in request don't hash to the same slot\r\n"));
                    } else if (error_code == REDIS_CLUSTER_REDIR_UNSTABLE) {
                        /* The request spawns mutliple keys in the same slot,
                         * but the slot is not "stable" currently as there is
                         * a migration or import in progress. */
                        addReplySds(c, sdsnew("-TRYAGAIN Multiple keys request during rehashing of slot\r\n"));
                    } else {
                        redisPanic("getNodeByQuery() unknown error.");
                    }
                    return REDIS_OK;
    
                    //3.1 命令針對的槽和鍵不是本節點處理的,進行轉向
                } else if (n != server.cluster->myself) {
                    flagTransaction(c);
                    // -<ASK or MOVED> <slot> <ip>:<port>
                    // 例如 -ASK 10086 127.0.0.1:12345
                    addReplySds(c, sdscatprintf(sdsempty(),
                                                "-%s %d %s:%d\r\n",
                                                (error_code == REDIS_CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
                                                hashslot, n->ip, n->port));
    
                    return REDIS_OK;
                }
    
                // 如果執行到這裏,說明鍵 key 所在的槽由本節點處理
                // 或者客戶端執行的是無參數命令
            }
        }
    
        /* Handle the maxmemory directive.
         *
         * First we try to free some memory if possible (if there are volatile
         * keys in the dataset). If there are not the only thing we can do
         * is returning an error. */
        //4 如果設置了最大內存,那麼檢查內存是否超過限制,並做相應的操作
        if (server.maxmemory) {
            //4.1 如果內存已超過限制,那麼嘗試通過刪除過期鍵來釋放內存
            int retval = freeMemoryIfNeeded();
            // 如果即將要執行的命令可能佔用大量內存(REDIS_CMD_DENYOOM)
            // 並且前面的內存釋放失敗的話
            // 那麼向客戶端返回內存錯誤
            if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
                flagTransaction(c);
                addReply(c, shared.oomerr);
                return REDIS_OK;
            }
        }    
        ....
    
    • 1處,查找命令,對應的函數指針(類似於java里的策略模式,根據命令,找對應的策略)
    • 2處,檢查,是否密碼正確
    • 3處,集群相關操作;
    • 3.1處,不是本節點處理,直接返回ask,指示客戶端轉向
    • 4處,判斷是否設置了maxMemory,這裏就是本文重點:設置了maxMemory時,內存淘汰策略
    • 4.1處,調用了下方的 freeMemoryIfNeeded

    接下來,深入4.1處:

    
    int freeMemoryIfNeeded(void) {
        size_t mem_used, mem_tofree, mem_freed;
        int slaves = listLength(server.slaves);
    
        /* Remove the size of slaves output buffers and AOF buffer from the
         * count of used memory. */
        // 計算出 Redis 目前佔用的內存總數,但有兩個方面的內存不會計算在內:
        // 1)從服務器的輸出緩衝區的內存
        // 2)AOF 緩衝區的內存
        mem_used = zmalloc_used_memory();
        if (slaves) {
    		...
        }
        if (server.aof_state != REDIS_AOF_OFF) {
            mem_used -= sdslen(server.aof_buf);
            mem_used -= aofRewriteBufferSize();
        }
    
        /* Check if we are over the memory limit. */
        //1 如果目前使用的內存大小比設置的 maxmemory 要小,那麼無須執行進一步操作
        if (mem_used <= server.maxmemory) return REDIS_OK;
    
        //2 如果佔用內存比 maxmemory 要大,但是 maxmemory 策略為不淘汰,那麼直接返回
        if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
            return REDIS_ERR; /* We need to free memory, but policy forbids. */
    
        /* Compute how much memory we need to free. */
        // 3 計算需要釋放多少字節的內存
        mem_tofree = mem_used - server.maxmemory;
    
        // 初始化已釋放內存的字節數為 0
        mem_freed = 0;
    
        // 根據 maxmemory 策略,
        //4 遍歷字典,釋放內存並記錄被釋放內存的字節數
        while (mem_freed < mem_tofree) {
            int j, k, keys_freed = 0;
    
            // 遍歷所有字典
            for (j = 0; j < server.dbnum; j++) {
                long bestval = 0; /* just to prevent warning */
                sds bestkey = NULL;
                dictEntry *de;
                redisDb *db = server.db + j;
                dict *dict;
    
                if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                    server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM) {
                    // 如果策略是 allkeys-lru 或者 allkeys-random 
                    //5 那麼淘汰的目標為所有數據庫鍵
                    dict = server.db[j].dict;
                } else {
                    // 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl 
                    //6 那麼淘汰的目標為帶過期時間的數據庫鍵
                    dict = server.db[j].expires;
                }
    
    
                /* volatile-random and allkeys-random policy */
                // 如果使用的是隨機策略,那麼從目標字典中隨機選出鍵
                if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                    server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM) {
                    de = dictGetRandomKey(dict);
                    bestkey = dictGetKey(de);
                }
                /* volatile-lru and allkeys-lru policy */
                //7 
                else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                         server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) {
                    struct evictionPoolEntry *pool = db->eviction_pool;
    
                    while (bestkey == NULL) {
                        // 8 
                        evictionPoolPopulate(dict, db->dict, db->eviction_pool);
                        /* Go backward from best to worst element to evict. */
                        for (k = REDIS_EVICTION_POOL_SIZE - 1; k >= 0; k--) {
                            if (pool[k].key == NULL) continue;
                            // 8.1
                            de = dictFind(dict, pool[k].key);
    
                            /* 8.2 Remove the entry from the pool. */
                            sdsfree(pool[k].key);
                            /* Shift all elements on its right to left. */
                            memmove(pool + k, pool + k + 1,
                                    sizeof(pool[0]) * (REDIS_EVICTION_POOL_SIZE - k - 1));
                            /* Clear the element on the right which is empty
                             * since we shifted one position to the left.  */
                            pool[REDIS_EVICTION_POOL_SIZE - 1].key = NULL;
                            pool[REDIS_EVICTION_POOL_SIZE - 1].idle = 0;
    
                            /* If the key exists, is our pick. Otherwise it is
                             * a ghost and we need to try the next element. */
                            // 8.3
                            if (de) {
                                bestkey = dictGetKey(de);
                                break;
                            } else {
                                /* Ghost... */
                                continue;
                            }
                        }
                    }
                }
    
                    /* volatile-ttl */
                    // 策略為 volatile-ttl ,從一集 sample 鍵中選出過期時間距離當前時間最接近的鍵
                else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                    ...
                }
    
                /* Finally remove the selected key. */
                // 8.4 刪除被選中的鍵
                if (bestkey) {
                    long long delta;
    
                    robj *keyobj = createStringObject(bestkey, sdslen(bestkey));
                    propagateExpire(db, keyobj);
                    /* We compute the amount of memory freed by dbDelete() alone.
                     * It is possible that actually the memory needed to propagate
                     * the DEL in AOF and replication link is greater than the one
                     * we are freeing removing the key, but we can't account for
                     * that otherwise we would never exit the loop.
                     *
                     * AOF and Output buffer memory will be freed eventually so
                     * we only care about memory used by the key space. */
                    // 計算刪除鍵所釋放的內存數量
                    delta = (long long) zmalloc_used_memory();
                    dbDelete(db, keyobj);
                    delta -= (long long) zmalloc_used_memory();
                    mem_freed += delta;
    
                    // 對淘汰鍵的計數器增一
                    server.stat_evictedkeys++;
    
                    notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",
                                        keyobj, db->id);
                    decrRefCount(keyobj);
                    keys_freed++;
    				...
                }
            }
    
            if (!keys_freed) return REDIS_ERR; /* nothing to free... */
        }
    
        return REDIS_OK;
    }
    
    • 1處,如果目前使用的內存大小比設置的 maxmemory 要小,那麼無須執行進一步操作

    • 2處,如果佔用內存比 maxmemory 要大,但是 maxmemory 策略為不淘汰,那麼直接返回

    • 3處,計算需要釋放多少字節的內存

    • 4處,遍歷字典,釋放內存並記錄被釋放內存的字節數

    • 5處,如果策略是 allkeys-lru 或者 allkeys-random 那麼淘汰的目標為所有數據庫鍵

    • 6處,如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl ,那麼淘汰的目標為帶過期時間的數據庫鍵

    • 7處,如果使用的是 LRU 策略, 那麼從 sample 鍵中選出 IDLE 時間最長的那個鍵

    • 8處,調用evictionPoolPopulate,該函數在下面講解,該函數的功能是,傳入一個鏈表,即這裏的db->eviction_pool,然後在函數內部,隨機找出n個key,放入傳入的鏈表中,並按照空閑時間排序,空閑最久的,放到最後。

      當該函數,返回后,db->eviction_pool這個鏈表裡就存放了我們要淘汰的key。

    • 8.1處,找到這個key,這個key,在後邊會被刪除

    • 8.2處,下面這一段,從db->eviction_pool將這個已經處理了的key刪掉

    • 8.3處,如果這個key,是存在的,則跳出循環,在後面8.4處,會被刪除

    • 8.4處,刪除這個key

    選擇哪些key作為被淘汰的key

    前面我們看到,在7處,如果為lru策略,則會進入8處的函數:

    evictionPoolPopulate。

    該函數的名稱為:填充(populate)驅逐(eviction)對象池(pool)。驅逐的意思,就是現在達到了maxmemory,沒辦法,只能開始刪除掉一部分元素,來騰空間了,不然新的put類型的命令,根本沒辦法執行。

    該方法的大概思路是,使用lru的時候,隨機找n個key,類似於抽樣,然後放到一個鏈表,根據空閑時間排序。

    具體看看該方法的實現:

    void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    

    其中,傳入的第三個參數,是要被填充的對象,在c語言中,習慣傳入一個入參,然後在函數內部填充或者修改入參對象的屬性。

    該屬性,就是前面說的那個鏈表,用來存放收集的隨機的元素,該鏈表中節點的結構如下:

    struct evictionPoolEntry {
        unsigned long long idle;    /* Object idle time. */
        sds key;                    /* Key name. */
    };
    

    該結構共2個字段,一個存儲key,一個存儲空閑時間。

    該鏈表中,共maxmemory-samples個元素,會按照idle時間長短排序,idle時間長的在鏈表尾部,(假設頭在左,尾在右)。

    void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
        int j, k, count;
        dictEntry *_samples[EVICTION_SAMPLES_ARRAY_SIZE];
        dictEntry **samples;
    
        /* Try to use a static buffer: this function is a big hit...
         * Note: it was actually measured that this helps. */
        if (server.maxmemory_samples <= EVICTION_SAMPLES_ARRAY_SIZE) {
            samples = _samples;
        } else {
            samples = zmalloc(sizeof(samples[0]) * server.maxmemory_samples);
        }
    
        /* 1 Use bulk get by default. */
        count = dictGetRandomKeys(sampledict, samples, server.maxmemory_samples);
    
    	// 2
        for (j = 0; j < count; j++) {
            unsigned long long idle;
            sds key;
            robj *o;
            dictEntry *de;
    
            de = samples[j];
            key = dictGetKey(de);
            /* If the dictionary we are sampling from is not the main
             * dictionary (but the expires one) we need to lookup the key
             * again in the key dictionary to obtain the value object. */
            if (sampledict != keydict) de = dictFind(keydict, key);
            // 3
            o = dictGetVal(de);
            // 4
            idle = estimateObjectIdleTime(o);
    
            /* 5  Insert the element inside the pool.
             * First, find the first empty bucket or the first populated
             * bucket that has an idle time smaller than our idle time. */
            k = 0;
            while (k < REDIS_EVICTION_POOL_SIZE &&
                   pool[k].key &&
                   pool[k].idle < idle)
                k++;
            
    		...
                
            // 6
            pool[k].key = sdsdup(key);
            pool[k].idle = idle;
        }
        if (samples != _samples) zfree(samples);
    }
    
    • 1處,獲取 server.maxmemory_samples個key,這裡是隨機獲取的,(dictGetRandomKeys),這個值,默認值為5,放到samples中

    • 2處,遍歷返回來的samples

    • 3處,調用如下宏,獲取val

      he的類型為dictEntry:

      /*
       * 哈希表節點
       */
      typedef struct dictEntry {
          
          // 鍵
          void *key;
      
          // 值
          union {
              // 1
              void *val;
              uint64_t u64;
              int64_t s64;
          } v;
      
          // 指向下個哈希表節點,形成鏈表
          struct dictEntry *next;
      
      } dictEntry;
      

      所以,這裏去

      robj *o; 
      
      o = dictGetVal(de);
      

      實際就是獲取其v屬性中的val,(1處):

      #define dictGetVal(he) ((he)->v.val)
      
    • 4處,準備計算該val的空閑時間

      我們上面3處,看到,獲取的o的類型為robj。我們現在看看怎麼計算對象的空閑時長:

      /* Given an object returns the min number of milliseconds the object was never
       * requested, using an approximated LRU algorithm. */
      unsigned long long estimateObjectIdleTime(robj *o) {
          //4.1 獲取系統的當前時間
          unsigned long long lruclock = LRU_CLOCK();
          // 4.2
          if (lruclock >= o->lru) {
              // 4.3
              return (lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;
          } else {
              return (lruclock + (REDIS_LRU_CLOCK_MAX - o->lru)) *
                          REDIS_LRU_CLOCK_RESOLUTION;
          }
      }
      

      這裏,4.1處,獲取系統的當前時間;

      4.2處,如果系統時間,大於對象的lru時間

      4.3處,則用系統時間減去對象的lru時間,再乘以單位,換算為毫秒,最終返回的單位,為毫秒(可以看註釋。)

      #define REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
      
    • 5處,這裏拿當前元素,和pool中已經放進去的元素,從第0個開始比較,如果當前元素的idle時長,大於pool中指針0指向的元素,則和pool中索引1的元素比較;直到條件不滿足為止。

      這句話意思就是,類似於冒泡,把當前元素一直往後冒,直到idle時長小於被比較的元素為止。

    • 6處,把當前元素放進pool中。

    經過上面的處理后,鏈表中存放了全部的抽樣元素,且ide時間最長的,在最右邊。

    對象還有字段存儲空閑時間?

    前面4處,說到,用系統的當前時間,減去對象的lru時間。

    大家看看對象的結構體

    typedef struct redisObject {
    
        // 類型
        unsigned type:4;
    
        // 編碼
        unsigned encoding:4;
    
        //1 對象最後一次被訪問的時間
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    
        // 引用計數
        int refcount;
    
        // 指向實際值的指針
        void *ptr;
    
    } robj;
    

    上面1處,lru屬性,就是用來存儲這個。

    創建對象時,直接使用當前系統時間創建

    robj *createObject(int type, void *ptr) {
    
        robj *o = zmalloc(sizeof(*o));
    
        o->type = type;
        o->encoding = REDIS_ENCODING_RAW;
        o->ptr = ptr;
        o->refcount = 1;
    
        /*1 Set the LRU to the current lruclock (minutes resolution). */
        o->lru = LRU_CLOCK();
        return o;
    }
    

    1處即是。

    robj *createEmbeddedStringObject(char *ptr, size_t len) {
        robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
        struct sdshdr *sh = (void*)(o+1);
    
        o->type = REDIS_STRING;
        o->encoding = REDIS_ENCODING_EMBSTR;
        o->ptr = sh+1;
        o->refcount = 1;
        // 1
        o->lru = LRU_CLOCK();
    
        sh->len = len;
        sh->free = 0;
        if (ptr) {
            memcpy(sh->buf,ptr,len);
            sh->buf[len] = '\0';
        } else {
            memset(sh->buf,0,len+1);
        }
        return o;
    }
    

    1處即是。

    每次查找該key時,刷新時間

    robj *lookupKey(redisDb *db, robj *key) {
    
        // 查找鍵空間
        dictEntry *de = dictFind(db->dict,key->ptr);
    
        // 節點存在
        if (de) {
            
    
            // 取出值
            robj *val = dictGetVal(de);
    
            /* Update the access time for the ageing algorithm.
             * Don't do it if we have a saving child, as this will trigger
             * a copy on write madness. */
            // 更新時間信息(只在不存在子進程時執行,防止破壞 copy-on-write 機制)
            if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
                // 1
                val->lru = LRU_CLOCK();
    
            // 返回值
            return val;
        } else {
    
            // 節點不存在
    
            return NULL;
        }
    }
    

    1處即是,包括get、set等各種操作,都會刷新該時間。

    仔細看下面的堆棧,set的,get同理:

    總結

    大家有沒有更清楚一些呢?

    總的來說,就是,設置了max-memory后,達到該內存限制后,會在處理命令時,檢查是否要進行內存淘汰;如果要淘汰,則根據maxmemory-policy的策略來。

    隨機選擇maxmemory-sample個元素,按照空閑時間排序,拉鏈表;挨個挨個清除。

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 老大吩咐的可重入分佈式鎖,終於完美的實現了!!!

    老大吩咐的可重入分佈式鎖,終於完美的實現了!!!

    重做永遠比改造簡單

    最近在做一個項目,將一個其他公司的實現系統(下文稱作舊系統),完整的整合到自己公司的系統(下文稱作新系統)中,這其中需要將對方實現的功能完整在自己系統也實現一遍。

    舊系統還有一批存量商戶,為了不影響存量商戶的體驗,新系統提供的對外接口,還必須得跟以前一致。最後系統完整切換之後,功能只運行在新系統中,這就要求舊系統的數據還需要完整的遷移到新系統中。

    當然這些在做這個項目之前就有預期,想過這個過程很難,但是沒想到有那麼難。原本感覺排期大半年,時間還是挺寬裕,現在感覺就是大坑,還不得不在坑裡一點點去填。

    哎,說多都是淚,不吐槽了,等到下次做完再給大家復盤下真正心得體會。

    回到正文,上篇文章Redis 分佈式鎖,咱們基於 Redis 實現一個分佈式鎖。這個分佈式鎖基本功能沒什麼問題,但是缺少可重入的特性,所以這篇文章小黑哥就帶大家來實現一下可重入的分佈式鎖。

    本篇文章將會涉及以下內容:

    • 可重入
    • 基於 ThreadLocal 實現方案
    • 基於 Redis Hash 實現方案

    先贊后看,養成習慣。微信搜索「程序通事」,關注就完事了~

    可重入

    說到可重入鎖,首先我們來看看一段來自 wiki 上可重入的解釋:

    若一個程序或子程序可以“在任意時刻被中斷然後操作系統調度執行另外一段代碼,這段代碼又調用了該子程序不會出錯”,則稱其為可重入(reentrant或re-entrant)的。即當該子程序正在運行時,執行線程可以再次進入並執行它,仍然獲得符合設計時預期的結果。與多線程併發執行的線程安全不同,可重入強調對單個線程執行時重新進入同一個子程序仍然是安全的。

    當一個線程執行一段代碼成功獲取鎖之後,繼續執行時,又遇到加鎖的代碼,可重入性就就保證線程能繼續執行,而不可重入就是需要等待鎖釋放之後,再次獲取鎖成功,才能繼續往下執行。

    用一段 Java 代碼解釋可重入:

    public synchronized void a() {
        b();
    }
    
    public synchronized void b() {
        // pass
    }
    

    假設 X 線程在 a 方法獲取鎖之後,繼續執行 b 方法,如果此時不可重入,線程就必須等待鎖釋放,再次爭搶鎖。

    鎖明明是被 X 線程擁有,卻還需要等待自己釋放鎖,然後再去搶鎖,這看起來就很奇怪,我釋放我自己~

    可重入性就可以解決這個尷尬的問題,當線程擁有鎖之後,往後再遇到加鎖方法,直接將加鎖次數加 1,然後再執行方法邏輯。退出加鎖方法之後,加鎖次數再減 1,當加鎖次數為 0 時,鎖才被真正的釋放。

    可以看到可重入鎖最大特性就是計數,計算加鎖的次數。所以當可重入鎖需要在分佈式環境實現時,我們也就需要統計加鎖次數。

    分佈式可重入鎖實現方式有兩種:

    • 基於 ThreadLocal 實現方案
    • 基於 Redis Hash 實現方案

    首先我們看下基於 ThreadLocal 實現方案。

    基於 ThreadLocal 實現方案

    實現方式

    Java 中 ThreadLocal可以使每個線程擁有自己的實例副本,我們可以利用這個特性對線程重入次數進行技術。

    下面我們定義一個ThreadLocal的全局變量 LOCKS,內存存儲 Map 實例變量。

    private static ThreadLocal<Map<String, Integer>> LOCKS = ThreadLocal.withInitial(HashMap::new);
    

    每個線程都可以通過 ThreadLocal獲取自己的 Map實例,Mapkey 存儲鎖的名稱,而 value存儲鎖的重入次數。

    加鎖的代碼如下:

    /**
     * 可重入鎖
     *
     * @param lockName  鎖名字,代表需要爭臨界資源
     * @param request   唯一標識,可以使用 uuid,根據該值判斷是否可以重入
     * @param leaseTime 鎖釋放時間
     * @param unit      鎖釋放時間單位
     * @return
     */
    public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
        Map<String, Integer> counts = LOCKS.get();
        if (counts.containsKey(lockName)) {
            counts.put(lockName, counts.get(lockName) + 1);
            return true;
        } else {
            if (redisLock.tryLock(lockName, request, leaseTime, unit)) {
                counts.put(lockName, 1);
                return true;
            }
        }
        return false;
    }
    

    ps: redisLock#tryLock 為上一篇文章實現的分佈鎖。

    由於公號外鏈無法直接跳轉,關注『程序通事』,回復分佈式鎖獲取源代碼。

    加鎖方法首先判斷當前線程是否已經已經擁有該鎖,若已經擁有,直接對鎖的重入次數加 1。

    若還沒擁有該鎖,則嘗試去 Redis 加鎖,加鎖成功之後,再對重入次數加 1 。

    釋放鎖的代碼如下:

    /**
     * 解鎖需要判斷不同線程池
     *
     * @param lockName
     * @param request
     */
    public void unlock(String lockName, String request) {
        Map<String, Integer> counts = LOCKS.get();
        if (counts.getOrDefault(lockName, 0) <= 1) {
            counts.remove(lockName);
            Boolean result = redisLock.unlock(lockName, request);
            if (!result) {
                throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "
                        + request);
            }
    
        } else {
            counts.put(lockName, counts.get(lockName) - 1);
        }
    }
    

    釋放鎖的時首先判斷重入次數,若大於 1,則代表該鎖是被該線程擁有,所以直接將鎖重入次數減 1 即可。

    若當前可重入次數小於等於 1,首先移除 Map中鎖對應的 key,然後再到 Redis 釋放鎖。

    這裏需要注意的是,當鎖未被該線程擁有,直接解鎖,可重入次數也是小於等於 1 ,這次可能無法直接解鎖成功。

    ThreadLocal 使用過程要記得及時清理內部存儲實例變量,防止發生內存泄漏,上下文數據串用等問題。

    下次咱來聊聊最近使用 ThreadLocal 寫的 Bug。

    相關問題

    使用 ThreadLocal 這種本地記錄重入次數,雖然真的簡單高效,但是也存在一些問題。

    過期時間問題

    上述加鎖的代碼可以看到,重入加鎖時,僅僅對本地計數加 1 而已。這樣可能就會導致一種情況,由於業務執行過長,Redis 已經過期釋放鎖。

    而再次重入加鎖時,由於本地還存在數據,認為鎖還在被持有,這就不符合實際情況。

    如果要在本地增加過期時間,還需要考慮本地與 Redis 過期時間一致性的,代碼就會變得很複雜。

    不同線程/進程可重入問題

    狹義上可重入性應該只是對於同一線程的可重入,但是實際業務可能需要不同的應用線程之間可以重入同把鎖。

    ThreadLocal的方案僅僅只能滿足同一線程重入,無法解決不同線程/進程之間重入問題。

    不同線程/進程重入問題就需要使用下述方案 Redis Hash 方案解決。

    基於 Redis Hash 可重入鎖

    實現方式

    ThreadLocal 的方案中我們使用了 Map 記載鎖的可重入次數,而 Redis 也同樣提供了 Hash (哈希表)這種可以存儲鍵值對數據結構。所以我們可以使用 Redis Hash 存儲的鎖的重入次數,然後利用 lua 腳本判斷邏輯。

    加鎖的 lua 腳本如下:

    ---- 1 代表 true
    ---- 0 代表 false
    
    if (redis.call('exists', KEYS[1]) == 0) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return 1;
    end ;
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return 1;
    end ;
    return 0;
    

    如果 KEYS:[lock],ARGV[1000,uuid]

    不熟悉 lua 語言同學也不要怕,上述邏輯還是比較簡單的。

    加鎖代碼首先使用 Redis exists 命令判斷當前 lock 這個鎖是否存在。

    如果鎖不存在的話,直接使用 hincrby創建一個鍵為 lock hash 表,並且為 Hash 表中鍵為 uuid 初始化為 0,然後再次加 1,最後再設置過期時間。

    如果當前鎖存在,則使用 hexists判斷當前 lock 對應的 hash 表中是否存在 uuid 這個鍵,如果存在,再次使用 hincrby 加 1,最後再次設置過期時間。

    最後如果上述兩個邏輯都不符合,直接返回。

    加鎖代碼如下:

    // 初始化代碼
    
    String lockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:lock.lua").openStream(), Charsets.UTF_8);
    lockScript = new DefaultRedisScript<>(lockLuaScript, Boolean.class);
    
    /**
     * 可重入鎖
     *
     * @param lockName  鎖名字,代表需要爭臨界資源
     * @param request   唯一標識,可以使用 uuid,根據該值判斷是否可以重入
     * @param leaseTime 鎖釋放時間
     * @param unit      鎖釋放時間單位
     * @return
     */
    public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
        long internalLockLeaseTime = unit.toMillis(leaseTime);
        return stringRedisTemplate.execute(lockScript, Lists.newArrayList(lockName), String.valueOf(internalLockLeaseTime), request);
    }
    

    Spring-Boot 2.2.7.RELEASE

    只要搞懂 Lua 腳本加鎖邏輯,Java 代碼實現還是挺簡單的,直接使用 SpringBoot 提供的 StringRedisTemplate 即可。

    解鎖的 Lua 腳本如下:

    -- 判斷 hash set 可重入 key 的值是否等於 0
    -- 如果為 0 代表 該可重入 key 不存在
    if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
        return nil;
    end ;
    -- 計算當前可重入次數
    local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
    -- 小於等於 0 代表可以解鎖
    if (counter > 0) then
        return 0;
    else
        redis.call('del', KEYS[1]);
        return 1;
    end ;
    return nil;
    

    首先使用 hexists 判斷 Redis Hash 表是否存給定的域。

    如果 lock 對應 Hash 表不存在,或者 Hash 表不存在 uuid 這個 key,直接返回 nil

    若存在的情況下,代表當前鎖被其持有,首先使用 hincrby使可重入次數減 1 ,然後判斷計算之後可重入次數,若小於等於 0,則使用 del 刪除這把鎖。

    解鎖的 Java 代碼如下:

    // 初始化代碼:
    
    
    String unlockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:unlock.lua").openStream(), Charsets.UTF_8);
    unlockScript = new DefaultRedisScript<>(unlockLuaScript, Long.class);
    
    /**
     * 解鎖
     * 若可重入 key 次數大於 1,將可重入 key 次數減 1 <br>
     * 解鎖 lua 腳本返回含義:<br>
     * 1:代表解鎖成功 <br>
     * 0:代表鎖未釋放,可重入次數減 1 <br>
     * nil:代表其他線程嘗試解鎖 <br>
     * <p>
     * 如果使用 DefaultRedisScript<Boolean>,由於 Spring-data-redis eval 類型轉化,<br>
     * 當 Redis 返回  Nil bulk, 默認將會轉化為 false,將會影響解鎖語義,所以下述使用:<br>
     * DefaultRedisScript<Long>
     * <p>
     * 具體轉化代碼請查看:<br>
     * JedisScriptReturnConverter<br>
     *
     * @param lockName 鎖名稱
     * @param request  唯一標識,可以使用 uuid
     * @throws IllegalMonitorStateException 解鎖之前,請先加鎖。若為加鎖,解鎖將會拋出該錯誤
     */
    public void unlock(String lockName, String request) {
        Long result = stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
        // 如果未返回值,代表其他線程嘗試解鎖
        if (result == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "
                    + request);
        }
    }
    

    解鎖代碼執行方式與加鎖類似,只不過解鎖的執行結果返回類型使用 Long。這裏之所以沒有跟加鎖一樣使用 Boolean ,這是因為解鎖 lua 腳本中,三個返回值含義如下:

    • 1 代表解鎖成功,鎖被釋放
    • 0 代表可重入次數被減 1
    • null 代表其他線程嘗試解鎖,解鎖失敗

    如果返回值使用 BooleanSpring-data-redis 進行類型轉換時將會把 null 轉為 false,這就會影響我們邏輯判斷,所以返回類型只好使用 Long

    以下代碼來自 JedisScriptReturnConverter

    相關問題

    spring-data-redis 低版本問題

    如果 Spring-Boot 使用 Jedis 作為連接客戶端,並且使用Redis Cluster 集群模式,需要使用 2.1.9 以上版本的spring-boot-starter-data-redis,不然執行過程中將會拋出:

    org.springframework.dao.InvalidDataAccessApiUsageException: EvalSha is not supported in cluster environment.
    

    如果當前應用無法升級 spring-data-redis也沒關係,可以使用如下方式,直接使用原生 Jedis 連接執行 lua 腳本。

    以加鎖代碼為例:

    public boolean tryLock(String lockName, String reentrantKey, long leaseTime, TimeUnit unit) {
        long internalLockLeaseTime = unit.toMillis(leaseTime);
        Boolean result = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Object innerResult = eval(connection.getNativeConnection(), lockScript, Lists.newArrayList(lockName), Lists.newArrayList(String.valueOf(internalLockLeaseTime), reentrantKey));
            return convert(innerResult);
        });
        return result;
    }
    
    private Object eval(Object nativeConnection, RedisScript redisScript, final List<String> keys, final List<String> args) {
    
        Object innerResult = null;
        // 集群模式和單點模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行
        // 集群
        if (nativeConnection instanceof JedisCluster) {
            innerResult = evalByCluster((JedisCluster) nativeConnection, redisScript, keys, args);
        }
        // 單點
        else if (nativeConnection instanceof Jedis) {
            innerResult = evalBySingle((Jedis) nativeConnection, redisScript, keys, args);
        }
        return innerResult;
    }
    

    數據類型轉化問題

    如果使用 Jedis 原生連接執行 Lua 腳本,那麼可能又會碰到數據類型的轉換坑。

    可以看到 Jedis#eval返回 Object,我們需要具體根據 Lua 腳本的返回值的,再進行相關轉化。這其中就涉及到 Lua 數據類型轉化為 Redis 數據類型。

    下面主要我們來講下 Lua 數據轉化 Redis 的規則中幾條比較容易踩坑:

    1、Lua number 與 Redis 數據類型轉換

    Lua 中 number 類型是一個雙精度的浮點數,但是 Redis 只支持整數類型,所以這個轉化過程將會丟棄小數位。

    2、Lua boolean 與 Redis 類型轉換

    這個轉化比較容易踩坑,Redis 中是不存在 boolean 類型,所以當Lua 中 true 將會轉為 Redis 整數 1。而 Lua 中 false 並不是轉化整數,而是轉化 null 返回給客戶端。

    3、Lua nil 與 Redis 類型轉換

    Lua nil 可以當做是一個空值,可以等同於 Java 中的 null。在 Lua 中如果 nil 出現在條件表達式,將會當做 false 處理。

    所以 Lua nil 也將會 null 返回給客戶端。

    其他轉化規則比較簡單,詳情參考:

    http://doc.redisfans.com/script/eval.html

    總結

    可重入分佈式鎖關鍵在於對於鎖重入的計數,這篇文章主要給出兩種解決方案,一種基於 ThreadLocal 實現方案,這種方案實現簡單,運行也比較高效。但是若要處理鎖過期的問題,代碼實現就比較複雜。

    另外一種採用 Redis Hash 數據結構實現方案,解決了 ThreadLocal 的缺陷,但是代碼實現難度稍大,需要熟悉 Lua 腳本,以及Redis 一些命令。另外使用 spring-data-redis 等操作 Redis 時不經意間就會遇到各種問題。

    幫助

    https://www.sofastack.tech/blog/sofa-jraft-rheakv-distributedlock/

    https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html

    最後說兩句(求關注)

    看完文章,哥哥姐姐們點個吧,周更真的超累,不知覺又寫了两天,拒絕白嫖,來點正反饋唄~。

    最後感謝各位的閱讀,才疏學淺,難免存在紕漏,如果你發現錯誤的地方,可以留言指出。如果看完文章還有其他不懂的地方,歡迎加我,互相學習,一起成長~

    最後謝謝大家支持~

    最最後,重要的事再說一篇~

    快來關注我呀~
    快來關注我呀~
    快來關注我呀~

    歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • [.NET 開源] 高性能的 Swifter.MessagePack 已發布,併發布新版本的 Swifter.Json 和 Swifter.Data。

    [.NET 開源] 高性能的 Swifter.MessagePack 已發布,併發布新版本的 Swifter.Json 和 Swifter.Data。

    抱歉各位朋友,由於各種私事公事,本應該在 19 年底發布的 Swifter.MessagePack 庫延遲了這麼久才發布,我深感抱歉。

    MsgPack 簡介

    MsgPack 一種非常輕巧的二進制數據交換格式,巧妙的設計讓它相比其他二進制數據格式更可讀,並且有着不錯的壓縮率和邏輯性能,是目前相當火熱的數據交換格式。

    Swifter.MessagePack 遵循 MsgPack 新的規範實現;相比 .NET 其他 MsgPack 序列化庫,Swifter.MessagePack 有着更好的性能,生成的內容更緊湊合理且更簡單易用。

    Nuget:Swifter.MessagePackSwifter.JsonSwifter.Data

    GitHub:Swifter.MessagePackSwifter.Json

    如果您想使用 Swifter 庫,請在 Nuget 上安裝/下載最新版本,如需單文件版本,請自行生成/合併。

     

    簡單使用 Swifter.MessagePack

    MessagePackFormatter 類內部還有十個方法重載,包括靜態和實例方法,總有一些適合您;這些方法都是線程安全的。

    更多使用方法請參考早期關於 Swifter.Json 的文章,GitHub 或 Wiki;學習交流進 Swifter 的 QQ 群:133630914(新群,歡迎加入)。

     

    Swifter 框架的特性

    (1) Swifter 可以運行在 .NET Framework 2.0+, .NET Core 2.0+, .NET Standard 2.0+, MONO JIT, MONO AOT, Xamarin.Android, Xamarin.iOS, Unity JIT 等平台/運行時上,Unity IL2CPP 運行時由於沒有我們測試環境,不知可否正常運行,更多信息請看下面的 AOT 說明

    (2) Swifter 有着深層的抽象封裝,這雖然帶來了一些性能和內存的損耗,但也獲得了更高的擴展性;Swifter.Json/Swifter.MessagePack/Swifter.Data 的可公用的代碼非常多,這使得在 Swifter 上實現一個新的序列化庫只需要編寫少量代碼即可實現,這是其他框架難實現的。

    (3) 雖然 Swifter 有很多接口和抽象編程,但是 Swifter 並沒有因此比其他的框架慢或內存佔用大,反比它們更快和更小內存佔用;這是因為 Swifter 從來都是使用更好算法和邏輯來獲取性能,而不是使用更直接的代碼獲取直接的性能。

    (4) 作為類庫開發者,我們深知每個人開發和測試的側重點都與他人不一樣,自己找出自己的問題太難,所以 Swifter.Json 和 Swifter.MessagePack 除了我們自己的測試單元之外, 還 “偷” 了 Newtonsoft, Neuecc 和 Spanjson 的 5000+ 個測試單元( 去除了 Newtonsoft 的部分測試單元);現已測試通過 4200+ 個,不通過 800+ 個是我們認為可以允許或是更加合理的行為。(不勞而獲的測試單元確實用着很爽,但事實是我們”搬”這些測試單元用了 3 天,無腦替換改到手指抽筋)

     

    Swifter.Json 和 Swifter.MessagePack

    (1) Swifter.MessagePack 和 Swifter.Json 一樣,都有着非常優異的性能和極小的額外內存分配。

    (2) Swifter.MessagePack 和 Swifter.Json 的 API 大致相同,如果使用者同時使用它們,那麼可以極小成本在它們之間切換。

    (3) 得益於 Swifter.Core 的強大數據映射,Swifter.MessagePack 和 Swifter.Json 都同時支持 .NET 上大多數常用的數據結構和類型。

    (4) Swifter.MessagePack 和 Swifter.Json 對重複引用的對象的表示方式不一樣,在開啟 MultiReferencingReference 配置項后,Swifter.Json 將使用 { “$ref”: “#/obj/1/target” } 來表示重複引用的對象,而 Swifter.MessagePack 使用對象在 MsgPack 內容的偏移量表示重複引用的對象;相比之下 Swifter.MessagePack 的方案更簡單性能更快,但是可讀性較差,不過說來 MsgPack 本來就是要專門的工具才能閱讀。

    (5) Swifter.MessagePack 在序列化基礎類型時,在保證精度不丟失的前提下,將大數據類型轉換為更小數據類型,以得到更緊湊的 MsgPack 內容(如將 double 123 轉換為 int 123,int 123 只需要 1 個字節即可表示,如果不做轉換則需要 9 個字節表示)。

    (6) Swifter.MessagePack 在序列化未知長度的集合時(如 Enumerable<T>),會將長度定義為四字節 (FixArray32),然後在寫入完成后把實際長度賦予這四字節長度;這樣雖然在較短的未知長度集合時,將產生 1-3 個 0;但是這避免了將未知長度的集合轉換為 List<T> 或 T[], 這提高了性能也減少了內存分配,這是不虧的(因為未知長度的集合很常用,如 Linq,DbDataReader 等)。

     

    新版本做了啥?

    (1) 主要是解決了已知 BUG,包括了 Issues 上提到的幾個。

    (2) 允許將 “” 值解析為 DateTime, int?, double 等基礎類型的默認值,但是需要啟用 EmptyStringAsDefault 配置項,默認未開啟。

    (3) 解決了 Swifter.Json 浮點數: float, double 失真的問題,並增加了 UseSystemFloatingPointsMethods 配置項使用系統的浮點數方法,此配置項的更多說明請看該配置項的註釋。

    (4) 增加了序列化的事件:ObjectFiltering 和 ArrayFiltering,這兩個事件可以對正在序列化中的 鍵/值 做處理和篩選,包括駝峰命名法,忽略一些值等。它們被放在 JsonFormatter 和 MessagePackFormatter 的實例裏面。

    (5) 增加了 .NET 對象的持久序列化和反序列化功能,這個功能將對象序列化為包含類型信息和字段值的內容,不包含邏輯信息;使用 SerializationBox<T> 盒子使用此功能。圖示:

    更多新增的功能請繼續看以下內容。

     

     

    AOT

    在 Swifter 新版本里,AOT 的 JIT 的界限更加明顯,由 VersionDifferences.IsSupportEmit 字段標識;當這個字段為 true 表示當前平台是 JIT 運行時,Swifter 將在一些類中使用 Emit 技術提高性能;當此字段為 false 時,Swifter 會完全不使用 Emit 技術。

    因我們設備有限,無法提供大規模的平台測試,但我們非常希望可以 Swifter 可以支持更多的平台,所以希望朋友們加 Swifter 交流 QQ 群:(133630914),在這裏我們可以更快的提供反饋。

     

    直接文檔讀取/寫入的 API

    通常情況下,將小型對象序列化為 Json/MsgPack 和將小型 Json/MsgPack 反序列化為對象是 .NET 程序中常見的操作,Swifter 也正以此為常用場景做優化,所以 Swifter 在對小型數據操作時性能最佳,且相比其他 Json/MsgPack 解析庫優勢明顯。

    但在大型數據下優勢減少,這主要原因是大型數據的存儲需要實體類或字典/集合存儲,創建/填充/遍歷這些對象消耗了大量資源(接口編程的損耗);所以 Swifter 提供了直接讀取/寫入的 API 來繞開了對存儲介質的操作,以更快更小損耗的讀寫大型數據。

    使用 JsonFormatter.CreateJsonReader/MessagePackFormatter.CreateMessagePackReader 函數來創建文檔讀取器,使用 JsonFormatter.CreateJsonWriter/MessagePackFormatter.CreateMessagePackWriter 函數來創建文檔寫入器。

    使用文檔讀取器完整的讀取一個 Json/MsgPack 文檔將比反序列化為對象快 4-8 倍!使用文檔寫入器生成文檔的性能與將實體類序列化為 Json/MsgPack 相差較小,前提是您已構建好了這些對象。

    讀取器演示:

    寫入器演示:

     

    擁有簡單預測數組的長度的能力

    Swifter 在對小型數組,部分集合寫入時,會根據數組的類型,來源(Data,Json,MsgPack 等),名稱等信息並結合之前的一些長度記錄,簡單的預測出新的數組的長度;在寫入完成后,如果預測長度與實際長度不符,則擴展或壓縮為實際長度;如果與實際長度相符,則不需要重新創建新數組。此能力有效提高反序列化小型數組和部分集合性能,並且減少額外內存分配。

    在其他高性能的 Json 解析庫,它們使用 ArrayPool<T> 同樣可以提高性能和減少內存分配;但是由於 Swifter 對兼容性的要求,使得我們不能使用 ArrayPool<T> 方案;在數組的長度比較穩定的情況下,我們的方案更好;但在數組長度非常不穩定的情況下,我們的方案可能仍需要 1-3 次的擴容/壓縮。

     

    假定有序的對象反序列化

    Swifter.Json 和 Swifter.MessagePack 都支持了假定有序的對象反序列化,當一個 Json/MsgPack 的對象與當前的實體類對象的字段順序一致時,將有效提升反序列化性能。

    此操作默認不開啟,可以使用 AsOrderedObjectDeserialize 配置項開啟。

     

    高性能的反射封裝

    Swifter.Core 里提供了一些對反射封裝的類,它們放在 Swifter.Reflection 命名空間下;這些類型主要功能就是提高了系統反射的性能;XObjectRW 正是使用它們實現不依賴 Emit 的高性能對象讀寫器。

    雖然放棄一些安全性檢查可以提高更多的性能,但是我們並沒有這麼做;我們仍然有類型安全檢查和防溢出檢查(事實上讀寫字段和屬性大多數的損害都在這裏,如果去掉這些檢查將得到上百倍的性能;事實上這些檢查只起到了提示程序員不能這麼做的作用,程序實際運行時這些檢查無意義)。

     

    高效的数字 ToString 和 Parse 方法

    Swifter.Core 提供了一些高性能数字算法,包括 Int64, UInt64, Double, Single, Decimal 的 Parse 和 ToString 算法,它們被放在 Swifter.Tools.NumberHelper 里,這些算法被應用與 Swifter.Json 和一些其他地方,這些算法支持 2-64 進制。

     

    XConvert 萬能類型轉換器

    Swifter.Tools.XConvert.Convert<TSource, TDestination> 是一個功能強大的萬能類型轉換函數,它在初始化時嘗試以下方式獲取合適的轉換函數:

    (1) 包含在 System.Convert 里的基礎轉換函數;

    (2) 類型兼容的隱式轉換(如:從子類轉換為父類,從 Int32 轉換為 Int64,從 Int64 轉換為 Double)。

    (3) 原類型和目標類型中的 static implicit operator (隱式轉換) 函數。

    (4) 原類型中的 ToXXX 實例函數。

    (5) 目標類型中的 Parse 和 ValueOf 靜態函數。

    (7) 目標類型的構造函數。

    (8) 原類型和目標類型中的 static explicit operator (顯式轉換) 函數。

    (9) 當以上方法都沒有找到合適函數時,將使用 (TDestination)(object)value 進行強制轉換。

    簡單示例:

     

    性能測試

    ServiceStack.Json, Jil, LitJson, NetJson 等庫因為出錯太多未展示出來;如果有需要,您可以到 GitHub 上自行克隆/修改/運行,已收錄了 .NET 的大多數 Json 序列化庫。

     

    更多實用功能等你發現…

    Swifter.Core 還提供了許許多多的工具類,包括反射,委託,類型轉換,字符串,加密,哈希,数字,日期,數組和集合等工具,它們被放在 Swifter.Tools 命名空間下,您可以使用它們來提高開發效率和運行效率。

    Swifter.RW 命名空間是整個 Swifter 框架的核心,它主要邏輯是:從讀取器中讀取值,寫入到寫入器中;如:從 JsonReader 讀取值到 ObjectWriter 或 DictionaryWriter 中;熟悉它們就等於精通了 Swifter 框架。

    Swifter.Json/Swifter.MessagePack 有一個非常重要的配置項 JsonFormatterOptions/MessagePackFormatterOptions;使用前建議先閱讀它們,以配置更適合您系統的序列化和反序列化方案。

     

    最後附上 Swifter.Data 的簡介

    Swifter.Data 是一個小型的 ORM 工具,它相比 Dapper 性能要快一些,功能要強大一些。

     

    感謝閱讀

    本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※別再煩惱如何寫文案,掌握八大原則!

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※超省錢租車方案

    ※教你寫出一流的銷售文案?

    網頁設計最專業,超強功能平台可客製化

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!

  • 為疫苗研發犧牲? 保育團體憂:25萬尾鯊魚恐間接受害

    摘錄自2020年9月29日自由時報報導

    為盡速結束武漢肺炎(COVID-19)疫情所帶來的災難,各國無不卯足全力研發疫苗,然而近期有研發廠商為了讓疫苗的效果穩定,決定在疫苗內添加高效的醫學物質「鯊烯」(Squalene),然而鯊烯這種物質相當稀少,美國有鯊魚保育團體推估,光是製造提供給美國使用的疫苗量,恐就要殺死超過2.1萬條鯊魚,若是全世界範圍,「可能要殺害25萬條鯊魚」。

    綜合外媒報導,「鯊烯」主要從鯊魚的肝油中提煉而出,在醫學與美容方面都是極為重要的高效素質,不僅可以滋潤皮膚,也可以提高、增強免疫力,加在疫苗中可使疫苗的效力提升,然而鯊魚肝油中提煉出的鯊烯十分稀少,平均一噸的鯊烯大約需要3000條鯊魚。

    英國製藥大廠葛蘭素史克(GlaxoSmithKline)專門生產用於流感疫苗中的以鯊烯製成的佐劑,而葛蘭素史克曾表示,將在2021年生產10億個以鯊烯製成的「武漢肺炎疫苗用佐劑」,引起保育團體的憂慮;位在美國加州的鯊魚保育團體鯊魚同盟(Shark Allies)表示,如果全世界的人都會接種這種加入佐劑的疫苗,「那可能要殺害25萬條鯊魚」,如果再算上二次接種,數量還會翻倍到50萬條。

    海洋
    國際新聞
    鯊魚

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※帶您來了解什麼是 USB CONNECTOR  ?

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    ※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

    ※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※教你寫出一流的銷售文案?

  • 保護全球最美星空 智利環團提告商業大樓「污染天空」

    摘錄自2020年9月28日奇摩新聞報導

    智利北部的阿他加馬沙漠曾獲選BBC全球十大最美暗夜星空,入夜後整片的星空美不勝收,吸引了各種追星者和天文學家,因此聚集大量觀星者的巨型望遠鏡,幾乎半數的世界天文觀測站都在這。但現在都市的擴張和發展伴隨的光污染使星星黯淡許多,甚至使一些關鍵地區的天空退化超過10%。

    智利環保機構表示,將提告用「人造冷光」污染天空的公司,當地政府也打算修法,若業者減少光污染將有特別優惠,希望利用合法的力量和新的保護措施讓天空保持黑暗。但目前收到起訴和修正的公司都還未回覆,其他公司也都還在審理中。

    污染治理
    國際新聞
    智利
    光害
    星空

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

  • 中國湖北化工廠大爆炸 至少5死1傷

    摘錄自2020年9月28日自由時報報導

    中國湖北省天門市岳口鎮譚湖工業園區內一化工廠,今(28)日下午發生爆炸,現場黃煙亂竄疑似硝酸外洩,當地政府部門指出,目前事故已造成5死1傷。

    綜合中媒報導,湖北省應急管理廳指出,天門市應急管理局報告,今日下午2點15分左右,天門市岳口工業園天門楚天精細化工有限公司進行設備調試期間,發生板框壓力機爆炸,初步發現事故現場5人死亡、1人受傷。有目擊者稱現場疑似是硝酸外洩,導致竄出大量黃煙。

    天門市應急管理局表示,傷者已送醫救治,現場搜救工作仍在進行中,事故原因及過程仍有待釐清。

    污染治理
    國際新聞
    中國
    化工廠
    化工廠爆炸

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

    ※教你寫出一流的銷售文案?

    ※超省錢租車方案

  • 紅毛猩猩家園上動土惹議 印尼中資水壩遇武肺將延後三年動工

    環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

    ※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

    ※教你寫出一流的銷售文案?

  • 人類壓力步步進逼 全球13年間荒野損失面積相當於墨西哥

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

    ※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

    網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

    ※想知道最厲害的網頁設計公司"嚨底家"!

    ※別再煩惱如何寫文案,掌握八大原則!

    ※產品缺大量曝光嗎?你需要的是一流包裝設計!