FreeSql v0.11 幾個實用功能說明

FreeSql 開源發布快一年了,立志成為 .Net 平台方便好用的 ORM,倉庫地址:

隨着不斷的迭代更新,越來越穩定,也越來越強大。預計在一周年的時候(2020年1月1日)發布 1.0 正式版本。

金九銀十的日子過去了,在這個銅一般的月份里,鄙人做了幾個重大功能,希望對使用者開發提供更大的便利。

  • 一、Dto 映射查詢
  • 二、IncludeMany 聯級加載
  • 三、Where(a => true) 邏輯表達式解析優化
  • 四、SaveManyToMany 聯級保存多對多集合屬性
  • 五、遷移實體 – 到指定表名
  • 六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert
  • 七、ISelect.ToDelete 高級刪除
  • 八、全局過濾器

以下的代碼,先決定義代碼如下 :

IFreeSql fsql = new FreeSql.FreeSqlBuilder()
    .UseConnectionString(FreeSql.DataType.Sqlite, @"Data Source=|DataDirectory|\db1.db;Max Pool Size=10";)
    .UseAutoSyncStructure(true) //自動同步實體結構到數據庫
    .Build();

public class Blog
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int Rating { get; set; }
}

一、Dto 映射查詢

class Dto
{
    public Guid Id { get; set; }
    public string Url { get; set; }
    public int xxx { get; set; }
}

fsql.Select<Blog>().ToList<Dto>();
//SELECT Id, Url FROM Blog

fsql.Select<Blog>().ToList(a => new Dto { xxx = a.Rating} );
//SELECT Id, Url, Rating as xxx FROM Blog
//這樣寫,附加所有映射,再額外映射 xxx

fsql.Select<Blog>().ToList(a => new Blog { Id = a.Id }) 
//這樣寫,只查詢 id

fsql.Select<Blog>().ToList(a => new { a.Id }) 
//這樣寫,只查詢 id,返回匿名對象

映射支持單表/多表,是在查詢數據之前映射(不是先查詢所有字段再到內存映射)

查找規則,查找屬性名,會循環內部對象 _tables(join 查詢後會增長),以 主表優先查,直到查到相同的字段。

如:

A, B, C 都有 id,Dto { id, a1, a2, b1, b2 },A.id 被映射。也可以指定 id = C.id 映射。

友情提醒:在 dto 可以直接映射一個導航屬性

二、IncludeMany 聯級加載

之前已經實現,有設置關係,和未設置關係 的導航集合屬性聯級加載。

有設置關係的(支持一對多、多對多):

fsql.Select<Tag>().IncludeMany(a => a.Goods).ToList();

未設置關係的,臨時指定關係(只支持一對多):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Where(b => b.TagId == a.Id));

只查詢每項子集合的前幾條數據,避免像EfCore加載所有數據導致IO性能低下(比如某商品下有2000條評論):

fsql.Select<Goods>().IncludeMany(a => a.Comment.Take(10));

上面已有的 IncludeMany 功能還不夠自由靈活。

新功能1:在 Dto 上做映射 IncludeMany

老的 IncludeMany 限制只能在 ISelect 內使用,必須要先查上級數據,解決這個問題我們做了直接在 Dto 上做映射:

查詢 Goods 商品表,分類1、分類2、分類3 各10條數據

//定義臨時類,也可以是 Dto 類
class Dto {
    public int TypeId { get; set; }
    public List<Goods > GoodsList { get; set; }
}

var dto = new [] { 1,2,3 }.Select(a => new Dto { TypeId = a }).ToList();
dto.IncludeMany(d => d.GoodsList.Take(10).Where(gd => gd.TypeId == d.TypeId));

//執行后,dto 每個元素.Vods 將只有 10條記錄

現在 IncludeMany 不再是 ISelect 的專利,普通的 List<T> 也可以用它來貪婪加載數據,並準確填充到內部各元素中。

新功能2:查詢子集合表的指定字段

老的 IncludeMany 限制只能查子表的所有字段,子表過段多過的話比較浪費 IO 性能。

新功能可以設置子集合返回部分字段,避免子集合字段過多的問題。

fsql.Select<Tag>().IncludeMany(a => a.Goods.Select(b => new Goods { Id = b.Id, Title = b.Title }));
//只查詢 goods 表 id, title 字段,再作填充

三、Where(a => true) 邏輯表達式解析優化

相信很多 ORM 解析表達式的時候處理不了這個問題,我們之前已經解決了 99%。

這個月發現還有一餘孽未清,發現問題后及時解決了,並增加單元測試代碼以絕後患。

四、SaveManyToMany 聯級保存多對多集合屬性

在此之前,FreeSql.DbContext 和 倉儲實現,已經實現了聯級保存功能,如下:

聯級保存功能可實現保存對象的時候,將其【OneToMany】、【ManyToMany】導航屬性集合也一併保存。

全局關閉:

fsql.SetDbContextOptions(opt => opt.EnableAddOrUpdateNavigateList = false);

局部關閉:

var repo = fsql.GetRepository<T>();
repo.DbContextOptions.EnableAddOrUpdateNavigateList = false;

新功能:

保存實體的指定【多對多】導航屬性,SaveManyToMany 方法實現在 BaseRepository、DbContext。

解決問題:當實體類導航數據過於複雜的時候,選擇關閉聯級保存的功能是明智之選,但是此時【多對多】數據保存功能寫起來非常繁瑣麻煩(因為要與現有數據對比后保存)。

var song = new Song { Id = 1 };
song.Tags = new List<Tag>();
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
song.Tags.Add(new Tag ...);
repo.SaveManyToMany(song, "Tags");
//輕鬆保存 song 與 tag 表的關聯

機制規則與聯級保存的【多對多】一樣,如下:

我們對中間表的保存是完整對比操作,對外部實體的操作只作新增(注意不會更新)

  • 屬性集合為空時,刪除他們的所有關聯數據(中間表)
  • 屬性集合不為空時,與數據庫存在的關聯數據(中間表)完全對比,計算出應該刪除和添加的記錄

五、遷移實體 – 到指定表名

fsql.CodeFirst.SyncStructure(typeof(Log), "Log_1"); //遷移到 Log_1 表
fsql.CodeFirst.SyncStructure(typeof(Log), "Log_2"); //遷移到 Log_2 表

在此功能上,我們對分表功能做了點升級,以下動作都會做遷移動作:

fsql.Select<Log>().AsTable((_, oldname) => $"{oldname}_1");
fsql.GetRepository<Log>(null, oldname => $"{oldname}_1");

六、MySql 特有功能 On Duplicate Key Update,和 Pgsql upsert

FreeSql 提供了多種插入或更新方法,v0.11 之前主要使用 FreeSql.Repository/FreeSql.DbContext 庫提供的方法實現。

FreeSql.Repository 之 InsertOrUpdate

此方法與 FreeSql.DbContext AddOrUpdate 方法功能一樣。

var repo = fsql.GetRepository<T>();
repo.InsertOrUpdate(實體);

如果內部的狀態管理存在數據,則更新。

如果內部的狀態管理不存在數據,同查詢數據庫,是否存在。

存在則更新,不存在則插入

缺點:不支持批量操作

新功能:MySql 特有功能 On Duplicate Key Update

FreeSql.Provider.MySql 和 FreeSql.Provider.MySqlConnector 在 v0.11.11 版本已支持 MySql 特有的功能,On Duplicate Key Update。

這個功能也可以實現插入或更新數據,並且支持批量操作。

class TestOnDuplicateKeyUpdateInfo
{
    [Column(IsIdentity = true)]
    public int id { get; set; }
    public string title { get; set; }
    public DateTime time { get; set; }
}

var item = new TestOnDuplicateKeyUpdateInfo { id = 100, title = "title-100", time = DateTime.Parse("2000-01-01") };
fsql.Insert(item)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`, `time`) VALUES(100, 'title-100', '2000-01-01 00:00:00.000')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = VALUES(`time`)

OnDuplicateKeyUpdate() 之後可以調用的方法:

方法名 描述
IgnoreColumns 忽略更新的列,機制和 IUpdate.IgnoreColumns 一樣
UpdateColumns 指定更新的列,機制和 IUpdate.UpdateColumns 一樣
Set 手工指定更新的列,與 IUpdate.Set 功能一樣
SetRaw 作為 Set 方法的補充,可傳入 SQL 字符串
ToSql 返回即將執行的 SQL 語句
ExecuteAffrows 執行,返回影響的行數

IInsert 與 OnDuplicateKeyUpdate 都有 IgnoreColumns、UpdateColumns 方法。

當插入實體/集合實體的時候,忽略了 time 列,代碼如下:

fsql.Insert(item)
    .IgnoreColumns(a => a.time)
    .NoneParameter()
    .OnDuplicateKeyUpdate().ToSql();
//INSERT INTO `TestOnDuplicateKeyUpdateInfo`(`id`, `title`) VALUES(200, 'title-200')
//ON DUPLICATE KEY UPDATE
//`title` = VALUES(`title`), 
//`time` = '2000-01-01 00:00:00.000'

我們發現,UPDATE time 部分變成了常量,而不是 VALUES(`time`),機制如下:

當 insert 部分中存在的列,在 update 中將以 VALUES(`字段`) 的形式設置;

當 insert 部分中不存在的列,在 update 中將為常量形式設置,當操作實體數組的時候,此常量為 case when … end 執行(與 IUpdate 一樣);

新功能2:PostgreSQL 特有功能 On Conflict Do Update

使用方法 MySql OnDuplicateKeyUpdate 大致相同。

七、ISelect.ToDelete 高級刪除

默認 IDelete 不支持導航對象,多表關聯等。ISelect.ToDelete 可將查詢轉為刪除對象,以便支持導航對象或其他查詢功能刪除數據,如下:

fsql.Select<T1>().Where(a => a.Options.xxx == 1).ToDelete().ExecuteAffrows();

注意:此方法不是將數據查詢到內存循環刪除,上面的代碼產生如下 SQL 執行:

DELETE FROM `T1` WHERE id in (select a.id from T1 a left join Options b on b.t1id = a.id where b.xxx = 1)

複雜刪除使用該方案的好處:

  • 刪除前可預覽測試數據,防止錯誤刪除操作;
  • 支持更加複雜的刪除操作(IDelete 默認只支持簡單的操作),甚至在 ISelect 上使用 Limit(10) 將只刪除附合條件的前 10條記錄;

還有 ISelect.ToUpdate 高級更新數據功能,使用方法類似

八、全局過濾器

FreeSql 基礎層實現了 Select/Update/Delete 可設置的全局過濾器功能。

public static AsyncLocal<Guid> TenantId { get; set; } = new AsyncLocal<Guid>();

fsql.GlobalFilter
    .Apply<TestAddEnum>("test1", a => a.Id == TenantId.Value)
    .Apply<AuthorTest>("test2", a => a.Id == 111)
    .Apply<AuthorTest>("test3", a => a.Name == "11");

Apply 泛型參數可以設置為任何類型,當使用 Select/Update/Delete 方法時會進行過濾器匹配嘗試(try catch):

  • 匹配成功的,將附加 where 條件;
  • 匹配失敗的,標記下次不再匹配,避免性能損耗;

如何禁用?

fsql.Select<TestAddEnum>().ToList(); //所有生效
fsql.Select<TestAddEnum>().DisableGlobalFilter("test1").ToList(); //禁用 test1
fsql.Select<TestAddEnum>().DisableGlobalFilter().ToList(); //禁用所有

fsql.Update/Delete 方法效果同上。

注意:IFreeSql.GlobalFilter 與 倉儲過濾器 不是一個功能,可以同時生效

鳴謝

感謝反饋 bug 的朋友!

倉庫地址:

請移步更新日誌:https://github.com/2881099/FreeSql/wiki/%e6%9b%b4%e6%96%b0%e6%97%a5%e5%bf%97

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

cocos creator 3D | 螞蟻莊園運動會星星球

上一篇文章寫了一個簡易版的螞蟻莊園登山賽,有小夥伴留言說想要看星星球的,那麼就寫起來吧!

效果預覽

配置環境 cocos creator 3d 1.0.0

小球點擊

3d里節點無法用 cc.Node.EventType.TOUCH_START 監聽。最終在論壇上找到一個 raycast 解決方法。參考代碼如下。

start() {
    systemEvent.on(SystemEventType.TOUCH_START, this.onClickBall, this);
}
private _ray = new geometry.ray();
private onClickBall(touch: Touch, event: EventTouch) {
    const pos = touch.getLocation();
    this.camera.screenPointToRay(pos.x, pos.y, this._ray);
    const result: { node: Node }[] = this.node_ball_click.scene.renderScene['raycast'](this._ray);
    if (result.some((i) => {
        if (i.node === this.node_ball_click) {
            return true;
        }
    })) {
        //點擊到小球處理邏輯
    }
}

其中 result 返回的是一個包含node節點的結果數組。獲取后需要判斷一下是否為小球節點。

據說這個方案消耗性能比較大,後續應該會有更好的解決方案。

動畫系統

採用了編輯器的動畫編輯器,對需要部分增加動畫效果。由於我的資源是網上找的,那隻雞有些身體部分切割的不好,所以小雞的動畫比較差一些。

需要注意的是動畫編輯器里的rotation屬性,與節點里的屬性面板的rotation對應不上,而應該採用eulerAngles的屬性。

據說後續版本會處理?

小球軌跡

採用tween控制小球坐標數值,先移動到最高點,然後再移動到最低點。

在運動軌跡中加入一些隨機值,就可以達到不同位置的效果啦。

tweenUtil(this._node_balll_pos)
    .stop()
    .to(time, new math.Vec3((this.node_ball.position.x + BALL_INIT_X) / 2, BALL_MAX_Y * (0.8 + 0.2 * Math.random()), targetZ / 2))
    .to(time, new math.Vec3(BALL_GAMEOVER_X, BALL_MIN_Y, targetZ))
    .start();

小結

完成這個小功能主要遇到的問題是3d節點點擊事件,和動畫系統的rotation的問題。不過這些都在論壇里找到了相應的解決方法。

以上就是我最新的學習成果!如有問題或新的想法歡迎留言!我有了好想法會第一時間分享給大家的!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?

Javascript模塊化開發2——Gruntfile.js詳解

一、grunt模塊簡介

grunt插件,是一種npm環境下的自動化工具。對於需要反覆重複的任務,例如壓縮、編譯、單元測試、linting等,自動化工具可以減輕你的勞動,簡化你的工作。grunt模塊根據Gruntfile.js文件中的配置進行任務。
如果在package.json中定義如下命令:

"scripts": {
    "build": "npm install && grunt"
}

因為運行npm run build會先安裝devDependencies中定義的一些模塊,則運行npm run build這個命令相當於做如下操作:

  • npm install grunt-cli -g
  • npm install
  • grunt

二、gruntfile.js的結構:

  • “wrapper” 函數
  • 項目和任務配置
  • 加載 grunt 插件和任務
  • 自定義任務

三、”wrapper” 函數

每一份 Gruntfile.js(和grunt插件)都遵循同樣的格式,你所書寫的Grunt代碼必須放在此函數內:

module.exports = function(grunt){
         //do grunt-related things in here
}

四、項目和任務配置

大部分的Grunt任務都依賴某些配置數據,我們通過grunt.initConfig 方法來配置Grunt任務的參數。
grunt.initConfig 方法的參數是一個JSON對象,你可以在這個配置對象中存儲任意的數據。此外,由於這本身就是JavaScript,你還可以在這裏使用任意的有效的JS代碼。甚至你可以用<% %>模板字符串來引用已經配置過的屬性,例如:

// 項目和任務配置
grunt.initConfig({
  pkg: grunt.file.readJSON('package.json'), //獲取 package.json 中的元數據(js代碼)
  proj:{
    name:'hello',
    description:'a hello demo'
  },
  hello: {
    options: {
      name: '<%= proj.name %>'  //用<% %>模板字符串匹配hello
    },
    srcs: ['1.txt', '2.txt']
  }
});

在grunt.initConfig 方法中配置的屬性,在任務模塊中,可用grunt.config方法進行訪問,例如:

grunt.config("proj.name");

另外,grunt任務模塊會自動根據任務名來提取配置對象中和任務名對應的屬性,比如定義任務hello,則在配置對象對應的屬性”hello”中配置任務執行函數中所需用到的配置和數據。

五、加載grunt插件任務

為了減少重複勞動,我們可以加載已有的插件任務。

1.加載自己私有的grunt插件

可將自己定義的一些task腳本放在同一個目錄下,通過grunt.loadTasks方法從指定目錄加載該目錄下所有的grunt任務腳本。

2.加載在npm中發布的grunt插件

像 grunt-contrib-copy和grunt-contrib-uglify這些常用的任務都已經以grunt插件的形式被開發出來了,且被發布在npm公開庫中,只要在 package.json 文件中將需要使用的插件列在dependency中,並通過npm install安裝之後,就可以直接加載該任務。

// 加載能夠提供"copy"任務的插件。
grunt.loadNpmTasks('grunt-contrib-copy');

3.直接加載所有以”grunt-“打頭的插件

npm上有個load-grunt-tasks插件可以用來加載dependency列表中所有以”grunt-“打頭的插件。
將需要使用的”grunt-“打頭的插件列在dependency中,然後在Gruntfile.js中進行調用。

//Load grunt tasks from NPM packages
load-grunt-tasks

六、自定義任務

1.直接定義任務的行為

grunt.registerTask('hello', 'Show some msg', function() {
  console.log(this.options().name); //輸出hello
});

2.定義為任務列表

可以將一個任務定義為一系列任務的組合,這一系列任務將按照順序執行。

grunt.registerTask('dothings', 'copy and Show some msg', ['copy','hello']);

3.定義默認任務

通過定義 default 任務,可以讓Grunt默認執行一個或多個任務。執行 grunt 命令時如果不指定一個任務的話,將會執行默認任務。如進行下面定義的話執行grunt 相當於執行grunt hello。

grunt.registerTask('default', ['hello']);

4.定義複合任務

registerMultiTask方法可以定義一個複合任務,複合任務將會對grunt.initConfig 方法中配置的相應屬性中除了options外定義的屬性依次作為target:data對進行處理。

module.exports = function(grunt) {
    grunt.initConfig({
        Log: {
            options: {
                sep: ';'
            },
            srcs: ['1.txt', '2.txt'],
            dests: ['d1.txt', 'd2.txt']
        }
    });
    grunt.registerMultiTask("Log", function() {
        var options = this.options({ sep: '&' });
        console.log(this.target); 
        console.log(this.data.join(options.sep));
    });
};

執行grunt Log將會輸出:

Running “Log:srcs” (Log) task
srcs
1.txt;2.txt
Running “Log:dests” (Log) task
dests
d1.txt;d2.txt

定義任務行為Tips

1.任務內部可以執行其他的任務。

grunt.registerTask('mytask', function() {
  grunt.task.run('task1', 'task2');
  // Or:
  grunt.task.run(['task1', 'task2']);
});

2.定義異步任務

grunt.registerTask('mytask', function() {
  var done = this.async();
  //do something
  done();
});

3.當任務失敗時,所有後續任務都將終止

在任務中,當執行失敗,可以return false來表明當前任務執行失敗,一般,多個任務按順序執行,如果有任務失敗時,所有後續任務都將終止。可以通過在命令行后加上–force來使有任務失敗時,後續任務能繼續進行。

4.任務中檢查前置任務狀態

有些任務可以依賴於其他任務的成功執行。通過grunt.task.requires方法來檢查其前置任務是否已經執行,並且沒有失敗。

5.任務中檢查配置屬性

可以用方法grunt.task.requiresConfig指定一個或者多個字符串或者數組的配置屬性為必需的。如果一個或多個必需的配置屬性缺失,就通知系統當前任務失敗。

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

【其他文章推薦】

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

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

※大陸寄台灣空運注意事項

※大陸海運台灣交貨時間多久?

go中的數據結構-接口interface

1. 接口的基本使用

  golang中的interface本身也是一種類型,它代表的是一個方法的集合。任何類型只要實現了接口中聲明的所有方法,那麼該類就實現了該接口。與其他語言不同,golang並不需要顯式聲明類型實現了某個接口,而是由編譯器和runtime進行檢查。

聲明

 1 type 接口名 interface{
 2     方法1
 3     方法2
 4     ...
 5    方法n 
 6 }
 7 type 接口名 interface {
 8     已聲明接口名1
 9     ...
10     已聲明接口名n
11 }
12 type iface interface{
13     tab *itab
14     data unsafe.Pointer
15 }

  接口自身也是一種結構類型,只是編譯器對其做了很多限制:

  • 不能有字段
  • 不能定義自己的方法
  • 只能聲明方法,不能實現
  • 可嵌入其他接口類型
 1 package main
 2 
 3     import (
 4         "fmt"
 5     )
 6 
 7     // 定義一個接口
 8     type People interface {
 9         ReturnName() string
10     }
11 
12     // 定義一個結構體
13     type Student struct {
14         Name string
15     }
16 
17     // 定義結構體的一個方法。
18     // 突然發現這個方法同接口People的所有方法(就一個),此時可直接認為結構體Student實現了接口People
19     func (s Student) ReturnName() string {
20         return s.Name
21     }
22 
23     func main() {
24         cbs := Student{Name:"小明"}
25 
26         var a People
27         // 因為Students實現了接口所以直接賦值沒問題
28         // 如果沒實現會報錯:cannot use cbs (type Student) as type People in assignment:Student does not implement People (missing ReturnName method)
29         a = cbs       
30         name := a.ReturnName() 
31         fmt.Println(name) // 輸出"小明"
32     }

  如果一個接口不包含任何方法,那麼它就是一個空接口(empty interface),所有類型都符合empty interface的定義,因此任何類型都能轉換成empty interface。

  接口的值簡單來說,是由兩部分組成的,就是類型和數據,判斷兩個接口是相等,就是看他們的這兩部分是否相等;另外類型和數據都為nil才代表接口是nil。

1 var a interface{} 
2 var b interface{} = (*int)(nil)
3 fmt.Println(a == nil, b == nil) //true false

2. 接口嵌套

  像匿名字段那樣嵌入其他接口。目標類型方法集中必須擁有包含嵌入接口方法在內的全部方法才算實現了該接口。嵌入其他接口類型相當於將其聲明的方法集中導入。這就要求不能有同名方法,不能嵌入自身或循環嵌入。

 1 type stringer interfaceP{
 2      string() string
 3 }
 4 
 5 type tester interface {
 6     stringer
 7     test()
 8 }    
 9 
10 type data struct{}
11 
12 func (*data) test() {}
13 
14 func (data) string () string {
15     return ""
16 }
17 
18 func main() {
19     var d data 
20     var t tester = &d 
21     t.test()
22     println(t.string())
23 }

  超集接口變量可隱式轉換為子集,反過來不行。

3. 接口的實現

golang的接口檢測既有靜態部分,也有動態部分。

  • 靜態部分
    對於具體類型(concrete type,包括自定義類型) -> interface,編譯器生成對應的itab放到ELF的.rodata段,後續要獲取itab時,直接把指針指向存在.rodata的相關偏移地址即可。具體實現可以看golang的提交日誌CL 20901、CL 20902。
    對於interface->具體類型(concrete type,包括自定義類型),編譯器提取相關字段進行比較,並生成值

  • 動態部分
    在runtime中會有一個全局的hash表,記錄了相應type->interface類型轉換的itab,進行轉換時候,先到hash表中查,如果有就返回成功;如果沒有,就檢查這兩種類型能否轉換,能就插入到hash表中返回成功,不能就返回失敗。注意這裏的hash表不是go中的map,而是一個最原始的使用數組的hash表,使用開放地址法來解決衝突。主要是interface <-> interface(接口賦值給接口、接口轉換成另一接口)使用到動態生產itab。

interface的結構如下:

接口類型的結構interfacetype

 1 type interfacetype struct {
 2     typ     _type   
 3     pkgpath name   //記錄定義接口的包名
 4     mhdr    []imethod  //一個imethod切片,記錄接口中定義的那些函數。
 5 }
 6 
 7 // imethod表示接口類型上的方法
 8 type imethod struct {
 9     name nameOff // name of method
10     typ  typeOff // .(*FuncType) underneath
11 }

  nameOff 和 typeOff 類型是 int32 ,這兩個值是鏈接器負責嵌入的,相對於可執行文件的元信息的偏移量。元信息會在運行期,加載到 runtime.moduledata 結構體中。

4. 接口值的結構iface和eface

 為了性能,golang專門分了兩種interface,eface和iface,eface就是空接口,iface就是有方法的接口。

 1 type iface struct { 
 2     tab  *itab
 3     data unsafe.Pointer
 4 }
 5 
 6 type eface struct {
 7     _type *_type
 8     data  unsafe.Pointer
 9 }
10 
11 type itab struct {
12     inter *interfacetype   //inter接口類型
13     _type *_type   //_type數據類型
14     hash  uint32  //_type.hash的副本。用於類型開關。 hash哈希的方法
15     _     [4]byte
16     fun   [1]uintptr  // 大小可變。 fun [0] == 0表示_type未實現inter。 fun函數地址佔位符
17 }

  iface結構體中的data是用來存儲實際數據的,runtime會申請一塊新的內存,把數據考到那,然後data指向這塊新的內存。

itab中的hash方法拷貝自_type.hash;fun是一個大小為1的uintptr數組,當fun[0]為0時,說明_type並沒有實現該接口,當有實現接口時,fun存放了第一個接口方法的地址,其他方法一次往下存放,這裏就簡單用空間換時間,其實方法都在_type字段中能找到,實際在這記錄下,每次調用的時候就不用動態查找了。

4.1 全局的itab table

iface.go:

1 const itabInitSize = 512
2 
3 // 注意:如果更改這些字段,請在itabAdd的mallocgc調用中更改公式。
4 type itabTableType struct {
5     size    uintptr             // 條目數組的長度。始終為2的冪。
6     count   uintptr             // 當前已填寫的條目數。
7     entries [itabInitSize]*itab // really [size] large
8 }

  可以看出這個全局的itabTable是用數組在存儲的,size記錄數組的大小,總是2的次冪。count記錄數組中已使用了多少。entries是一個*itab數組,初始大小是512。

5. 接口類型轉換

  把一個具體的值,賦值給接口,會調用conv系列函數,例如空接口調用convT2E系列、非空接口調用convT2I系列,為了性能考慮,很多特例的convT2I64、convT2Estring諸如此類,避免了typedmemmove的調用。

 1 func convT2E(t *_type, elem unsafe.Pointer) (e eface) {
 2     if raceenabled {
 3         raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2E))
 4     }
 5     if msanenabled {
 6         msanread(elem, t.size)
 7     }
 8     x := mallocgc(t.size, t, true)
 9     // TODO: 我們分配一個清零的對象只是為了用實際數據覆蓋它。
10     //確定如何避免歸零。同樣在下面的convT2Eslice,convT2I,convT2Islice中。
11     typedmemmove(t, x, elem)
12     e._type = t
13     e.data = x
14     return
15 }
16 
17 func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
18     t := tab._type
19     if raceenabled {
20         raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
21     }
22     if msanenabled {
23         msanread(elem, t.size)
24     }
25     x := mallocgc(t.size, t, true)
26     typedmemmove(t, x, elem)
27     i.tab = tab
28     i.data = x
29     return
30 }
31 
32 func convT2I16(tab *itab, val uint16) (i iface) {
33     t := tab._type
34     var x unsafe.Pointer
35     if val == 0 {
36         x = unsafe.Pointer(&zeroVal[0])
37     } else {
38         x = mallocgc(2, t, false)
39         *(*uint16)(x) = val
40     }
41     i.tab = tab
42     i.data = x
43     return
44 }
45 
46 func convI2I(inter *interfacetype, i iface) (r iface) {
47     tab := i.tab
48     if tab == nil {
49         return
50     }
51     if tab.inter == inter {
52         r.tab = tab
53         r.data = i.data
54         return
55     }
56     r.tab = getitab(inter, tab._type, false)
57     r.data = i.data
58     return
59 }

  可以看出:

  • 具體類型轉空接口,_type字段直接複製源的type;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 具體類型轉非空接口,入參tab是編譯器生成的填進去的,接口指向同一個入參tab指向的itab;mallocgc一個新內存,把值複製過去,data再指向這塊內存。
  • 對於接口轉接口,itab是調用getitab函數去獲取的,而不是編譯器傳入的。

對於那些特定類型的值,如果是零值,那麼不會mallocgc一塊新內存,data會指向zeroVal[0]。

5.1 接口轉接口

 1 func assertI2I2(inter *interfacetype, i iface) (r iface, b bool) {
 2     tab := i.tab
 3     if tab == nil {
 4         return
 5     }
 6     if tab.inter != inter {
 7         tab = getitab(inter, tab._type, true)
 8         if tab == nil {
 9             return
10         }
11     }
12     r.tab = tab
13     r.data = i.data
14     b = true
15     return
16 }
17 
18 func assertE2I(inter *interfacetype, e eface) (r iface) {
19     t := e._type
20     if t == nil {
21         // 顯式轉換需要非nil接口值。
22         panic(&TypeAssertionError{nil, nil, &inter.typ, ""})
23     }
24     r.tab = getitab(inter, t, false)
25     r.data = e.data
26     return
27 }
28 
29 func assertE2I2(inter *interfacetype, e eface) (r iface, b bool) {
30     t := e._type
31     if t == nil {
32         return
33     }
34     tab := getitab(inter, t, true)
35     if tab == nil {
36         return
37     }
38     r.tab = tab
39     r.data = e.data
40     b = true
41     return
42 }

  我們看到有兩種用法:

  • 返回值是一個時,不能轉換就panic。
  • 返回值是兩個時,第二個返回值標記能否轉換成功

  此外,data複製的是指針,不會完整拷貝值。每次都malloc一塊內存,那麼性能會很差,因此,對於一些類型,golang的編譯器做了優化。

5.2 接口轉具體類型

  接口判斷是否轉換成具體類型,是編譯器生成好的代碼去做的。我們看個empty interface轉換成具體類型的例子:

 1 var EFace interface{}
 2 var j int
 3 
 4 func F4(i int) int{
 5     EFace = I
 6     j = EFace.(int)
 7     return j
 8 }
 9 
10 func main() {
11     F4(10)
12 }

反彙編:

  go build -gcflags ‘-N -l’ -o tmp build.go

  go tool objdump -s “main.F4” tmp

  可以看彙編代碼:

1 MOVQ main.EFace(SB), CX       //CX = EFace.typ
2 LEAQ type.*+60128(SB), DX    //DX = &type.int
3 CMPQ DX, CX.                         //if DX == AX

  可以看到empty interface轉具體類型,是編譯器生成好對比代碼,比較具體類型和空接口是不是同一個type,而不是調用某個函數在運行時動態對比。

5.3 非空接口類型轉換

 1 var tf Tester
 2 var t testStruct
 3 
 4 func F4() int{
 5     t := tf.(testStruct)
 6     return t.i
 7 }
 8 
 9 func main() {
10     F4()
11 }
12 //反彙編
13 MOVQ main.tf(SB), CX   // CX = tf.tab(.inter.typ)
14 LEAQ go.itab.main.testStruct,main.Tester(SB), DX // DX = <testStruct,Tester>對應的&itab(.inter.typ)
15 CMPQ DX, CX //

  可以看到,非空接口轉具體類型,也是編譯器生成的代碼,比較是不是同一個itab,而不是調用某個函數在運行時動態對比。

6. 獲取itab的流程

  golang interface的核心邏輯就在這,在get的時候,不僅僅會從itabTalbe中查找,還可能會創建插入,itabTable使用容量超過75%還會擴容。看下代碼:

 1 func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
 2     if len(inter.mhdr) == 0 {
 3         throw("internal error - misuse of itab")
 4     }
 5 
 6     // 簡單的情況
 7     if typ.tflag&tflagUncommon == 0 {
 8         if canfail {
 9             return nil
10         }
11         name := inter.typ.nameOff(inter.mhdr[0].name)
12         panic(&TypeAssertionError{nil, typ, &inter.typ, name.name()})
13     }
14 
15     var m *itab
16 
17     //首先,查看現有表以查看是否可以找到所需的itab。
18     //這是迄今為止最常見的情況,因此請不要使用鎖。
19     //使用atomic確保我們看到該線程完成的所有先前寫入更新itabTable字段(在itabAdd中使用atomic.Storep)。
20     t := (*itabTableType)(atomic.Loadp(unsafe.Pointer(&itabTable)))
21     if m = t.find(inter, typ); m != nil {
22         goto finish
23     }
24 
25     // 未找到。抓住鎖,然後重試。
26     lock(&itabLock)
27     if m = itabTable.find(inter, typ); m != nil {
28         unlock(&itabLock)
29         goto finish
30     }
31 
32     // 條目尚不存在。進行新輸入並添加。
33     m = (*itab)(persistentalloc(unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize, 0, &memstats.other_sys))
34     m.inter = inter
35     m._type = typ
36     m.init()
37     itabAdd(m)
38     unlock(&itabLock)
39 finish:
40     if m.fun[0] != 0 {
41         return m
42     }
43     if canfail {
44         return nil
45     }
46     //僅當轉換時才會發生,使用ok形式已經完成一次,我們得到了一個緩存的否定結果。
47     //緩存的結果不會記錄,缺少接口函數,因此初始化再次獲取itab,以獲取缺少的函數名稱。
48     panic(&TypeAssertionError{concrete: typ, asserted: &inter.typ, missingMethod: m.init()})
49 }

  流程如下:

  • 先用t保存全局itabTable的地址,然後使用t.find去查找,這樣是為了防止查找過程中,itabTable被替換導致查找錯誤。
  • 如果沒找到,那麼就會上鎖,然後使用itabTable.find去查找,這樣是因為在第一步查找的同時,另外一個協程寫入,可能導致實際存在卻查找不到,這時上鎖避免itabTable被替換,然後直接在itaTable中查找。
  • 再沒找到,說明確實沒有,那麼就根據接口類型、數據類型,去生成一個新的itab,然後插入到itabTable中,這裏可能會導致hash表擴容,如果數據類型並沒有實現接口,那麼根據調用方式,該報錯報錯,該panic panic。

  這裏我們可以看到申請新的itab空間時,內存空間的大小是unsafe.Sizeof(itab{})+uintptr(len(inter.mhdr)-1)*sys.PtrSize,參照前面接受的結構,len(inter.mhdr)就是接口定義的方法數量,因為字段fun是一個大小為1的數組,所以len(inter.mhdr)-1,在fun字段下面其實隱藏了其他方法接口地址。

6.1 在itabTable中查找itab find

 1 func itabHashFunc(inter *interfacetype, typ *_type) uintptr {
 2     // 編譯器為我們提供了一些很好的哈希碼。
 3     return uintptr(inter.typ.hash ^ typ.hash)
 4 }
 5 
 6    // find在t中找到給定的接口/類型對。
 7    // 如果不存在給定的接口/類型對,則返回nil。
 8 func (t *itabTableType) find(inter *interfacetype, typ *_type) *itab {
 9     // 使用二次探測實現。
10      //探測順序為h(i)= h0 + i *(i + 1)/ 2 mod 2 ^ k。
11      //我們保證使用此探測序列擊中所有表條目。
12     mask := t.size - 1
13     h := itabHashFunc(inter, typ) & mask
14     for i := uintptr(1); ; i++ {
15         p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
16         // 在這裏使用atomic read,所以如果我們看到m!= nil,我們也會看到m字段的初始化。
17         // m := *p
18         m := (*itab)(atomic.Loadp(unsafe.Pointer(p)))
19         if m == nil {
20             return nil
21         }
22         if m.inter == inter && m._type == typ {
23             return m
24         }
25         h += I
26         h &= mask
27     }
28 }

  從註釋可以看到,golang使用的開放地址探測法,用的是公式h(i) = h0 + i*(i+1)/2 mod 2^k,h0是根據接口類型和數據類型的hash字段算出來的。以前的版本是額外使用一個link字段去連到下一個slot,那樣會有額外的存儲,性能也會差寫,在1.11中我們看到有了改進。

6.2 檢查並生成itab init

 1 // init用所有代碼指針填充m.fun數組m.inter / m._type對。 如果該類型未實現該接口,將m.fun [0]設置為0,並返回缺少的接口函數的名稱。
 2 //可以在同一m上多次調用此函數,即使同時調用也可以。
 3 func (m *itab) init() string {
 4     inter := m.inter
 5     typ := m._type
 6     x := typ.uncommon()
 7 
 8     // inter和typ都有按名稱排序的方法,
 9      //並且接口名稱是唯一的,
10      //因此可以在鎖定步驟中對兩者進行迭代;
11      //循環是O(ni + nt)而不是O(ni * nt)。
12     ni := len(inter.mhdr)
13     nt := int(x.mcount)
14     xmhdr := (*[1 << 16]method)(add(unsafe.Pointer(x), uintptr(x.moff)))[:nt:nt]
15     j := 0
16 imethods:
17     for k := 0; k < ni; k++ {
18         i := &inter.mhdr[k]
19         itype := inter.typ.typeOff(i.ityp)
20         name := inter.typ.nameOff(i.name)
21         iname := name.name()
22         ipkg := name.pkgPath()
23         if ipkg == "" {
24             ipkg = inter.pkgpath.name()
25         }
26         for ; j < nt; j++ {
27             t := &xmhdr[j]
28             tname := typ.nameOff(t.name)
29             if typ.typeOff(t.mtyp) == itype && tname.name() == iname {
30                 pkgPath := tname.pkgPath()
31                 if pkgPath == "" {
32                     pkgPath = typ.nameOff(x.pkgpath).name()
33                 }
34                 if tname.isExported() || pkgPath == ipkg {
35                     if m != nil {
36                         ifn := typ.textOff(t.ifn)
37                         *(*unsafe.Pointer)(add(unsafe.Pointer(&m.fun[0]), uintptr(k)*sys.PtrSize)) = ifn
38                     }
39                     continue imethods
40                 }
41             }
42         }
43         // didn't find method
44         m.fun[0] = 0
45         return iname
46     }
47     m.hash = typ.hash
48     return ""
49 }

  這個方法會檢查interface和type的方法是否匹配,即type有沒有實現interface。假如interface有n中方法,type有m中方法,那麼匹配的時間複雜度是O(n x m),由於interface、type的方法都按字典序排,所以O(n+m)的時間複雜度可以匹配完。在檢測的過程中,匹配上了,依次往fun字段寫入type中對應方法的地址。如果有一個方法沒有匹配上,那麼就設置fun[0]為0,在外層調用會檢查fun[0]==0,即type並沒有實現interface。

  這裏我們還可以看到golang中continue的特殊用法,要直接continue到外層的循環中,那麼就在那一層的循環上加個標籤,然後continue 標籤。

6.3 把itab插入到itabTable中 itabAdd

 1 // itabAdd將給定的itab添加到itab哈希表中。
 2 //必須保持itabLock。
 3 func itabAdd(m *itab) {
 4     // 設置了mallocing時,錯誤可能導致調用此方法,通常是因為這是在恐慌時調用的。
 5     //可靠地崩潰,而不是僅在需要增長時崩潰哈希表。
 6     if getg().m.mallocing != 0 {
 7         throw("malloc deadlock")
 8     }
 9 
10     t := itabTable
11     if t.count >= 3*(t.size/4) { // 75% 負載係數
12         // 增長哈希表。
13         // t2 = new(itabTableType)+一些其他條目我們撒謊並告訴malloc我們想要無指針的內存,因為所有指向的值都不在堆中。
14         t2 := (*itabTableType)(mallocgc((2+2*t.size)*sys.PtrSize, nil, true))
15         t2.size = t.size * 2
16 
17         // 複製條目。
18         //注意:在複製時,其他線程可能會尋找itab和找不到它。沒關係,他們將嘗試獲取Itab鎖,因此請等到複製完成。
19         if t2.count != t.count {
20             throw("mismatched count during itab table copy")
21         }
22         // 發布新的哈希表。使用原子寫入:請參閱getitab中的註釋。
23         atomicstorep(unsafe.Pointer(&itabTable), unsafe.Pointer(t2))
24         // 採用新表作為我們自己的表。
25         t = itabTable
26         // 注意:舊錶可以在此處進行GC處理。
27     }
28     t.add(m)
29 }
30 // add將給定的itab添加到itab表t中。
31 //必須保持itabLock。
32 func (t *itabTableType) add(m *itab) {
33     //請參閱註釋中的有關探查序列的註釋。
34     //將新的itab插入探針序列的第一個空位。
35     mask := t.size - 1
36     h := itabHashFunc(m.inter, m._type) & mask
37     for i := uintptr(1); ; i++ {
38         p := (**itab)(add(unsafe.Pointer(&t.entries), h*sys.PtrSize))
39         m2 := *p
40         if m2 == m {
41             //給定的itab可以在多個模塊中使用並且由於全局符號解析的工作方式,
42             //指向itab的代碼可能已經插入了全局“哈希”。
43             return
44         }
45         if m2 == nil {
46             // 在這裏使用原子寫,所以如果讀者看到m,它也會看到正確初始化的m字段。
47             // NoWB正常,因為m不在堆內存中。
48             // *p = m
49             atomic.StorepNoWB(unsafe.Pointer(p), unsafe.Pointer(m))
50             t.count++
51             return
52         }
53         h += I
54         h &= mask
55     }
56 }

  可以看到,當hash表使用達到75%或以上時,就會進行擴容,容量是原來的2倍,申請完空間,就會把老表中的數據插入到新的hash表中。然後使itabTable指向新的表,最後把新的itab插入到新表中。

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

git回退之git reset

參考

https://git-scm.com/book/zh/v2/Git-%E5%B7%A5%E5%85%B7-%E9%87%8D%E7%BD%AE%E6%8F%AD%E5%AF%86

https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified

https://git-scm.com/docs/git-reset

https://www.liaoxuefeng.com/wiki/896043488029600/897013573512192

從歷史記錄中刪除 參考 https://www.cnblogs.com/studywithallofyou/p/11772684.html https://www.cnblogs.com/studywithallofyou/p/11772844.html

前言

在使用git的時候,我們一般提倡是不允許回滾。對於問題的追蹤和項目的發展歷程而言歷史記錄都是有用的。並且為了節省一點存儲空間而丟失寶貴的代碼信息是不值當的。但是我們開發中,肯定會遇到特殊情況需要回退。比如確實操作錯了一步歷史提交,導致倉庫混亂污染或是內容丟失,我們需要回退到乾淨的一次提交,重新操作。

在git等所有的版本管理軟件中,刪除操作,只是增加一次記錄,內容並不會被刪掉,我們可以大膽的操作,這也符合版本倉庫的邏輯。只有特殊情況才需要真正的刪除,做這種操作的時候需要特別注意,因為一旦失誤,無法挽回。

工作流程

要想理解git reset,那麼就要搞清楚git倉庫管理流程:

我們修改完內容后,這些記錄單單是保存在我們本地目錄下,也就是工作目錄。如果丟了,就是丟了,無法找回。與普通磁盤上的文件一樣,除非到回收站找回。

這時,如果運行了add,那麼內容就被記錄到本地的暫存倉庫,也就是index。這時如果刪除文件,在暫存區的內容還存在,並沒有丟失。

如下圖,我們創建了一個文件4,這時文件4在本地。我們add到暫存倉庫,然後刪掉文件4,我們發現又多了一條記錄刪除記錄,原來的4還存在,我們可以commit。原來的文件4並沒有丟失。

運行commit之後,修改的內容就被保存到了倉庫,修改了HEAD(HEAD就是指向當前倉庫在哪一個提交歷史,不特殊修改,都指向最新的一次提交)。這時運行git status發現目錄是乾淨的。git裏面需要提交倉庫,暫存倉庫和本地目錄內容都完全一致,才是乾淨的,任何一個不一致都會有不同的提示。

比如上面的圖,提交的倉庫中沒有文件4,暫存倉庫中增加了4,所以显示綠色的提示內容,add了文件但是還沒有commit。但是本次又刪除了文件,本地目錄中的內容與暫存倉庫也不一樣,所以提示紅色的內容,因為算是警告,沒有add的內容是會丟失的。

修改

add

commit

git reset的三個選擇

理解了上面的流程,就可以理解git reset了。git reset就是分別逆向操作,也就是把HEAD(提交的倉庫)回退到一個指定歷史,把HEAD、index(暫存倉庫)都回退到指定歷史,把HEAD、index和本地目錄的內容都回退到一個指定歷史。

git reset –soft就是把HEAD回退到指定歷史。運行后結果如下

也就相當於我們add了修改的文件,本地目錄和暫存倉庫都已經一致了,就等待commit。如果我們commit,可以再次填寫commit記錄,也就實現了git commit –amend的功能。

git reset –mixed就是把HEAD和index都回退到指定的歷史。這個也是運行git reset不加參數時的默認規則。運行結果如下

相當於我們僅僅修改了文件,還沒做任何處理。

git reset –hard這個是把HEAD、index和本地目錄的內容都回退到指定的歷史記錄。做這一步操作的時候一定要小心,最好把所有的內容都提交,並且push到遠程或是拷貝一份。因為這個操作會重置本地的內容到一個指定歷史。

我們先運行git log,看到有三個提交歷史

運行git reset –hard HEAD~

我們發現倉庫是乾淨的,並且原來的3文件沒了,運行git log參看

提交的歷史記錄也沒有了。

這次是真的回退到了指定歷史,所有的記錄都不見了,我們可以開心的(真的嗎?)在原來一個乾淨的分支上繼續寫代碼了。

git reset –hard的後悔葯

世上有沒有後悔葯我不知道,但是git作為一個先進的分佈式管理器,卻有無限可能。如果你一不小心,腦袋發熱,運行了git reset –hard,但是發現不是你想要的,原來的記錄也沒了。怎麼辦?大腦瞬間充血,一片空白。不要慌,首先冷靜下來,然後運行git reflog,這個命令是告訴你你的每一次對倉庫操作的歷史記錄,如下

看一下最上面的幾條,第一條告訴你當前在e86d948這個提交記錄,通過reset切換過來的。對照上面的git log,可以發現這個是第二個提交歷史,也就是我們git reset –hard HEAD~的時候回退的分支。第二條記錄告訴我們當前是fc9fbc6分支,通過reset回退到這個分支的,我們可以參考上面的git log記錄,這個就是我們想要回去的分支。好了,有了記錄的哈希值,我們只需再運行一次git reset –hard,如下

查看一下git log

回來了。

git不會刪除任何已經提交到版本庫的內容,除非你非要專門特殊這樣做,並且為了再給你一次機會,像git reset這樣,就算你明確說明不要了,它也不會立馬刪除,除非超過一定時間或是你主動運行git gc等操作,把無用的,沒有關聯的內容刪掉。不然,那條記錄還是在本地倉庫,只不過它沒有被載入歷史的長河中。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?

MySQL InnoDB 鎖

MySQL 原理篇

數據準備:

/*
SQLyog Ultimate v12.09 (64 bit)
MySQL - 5.6.17 : Database - test
*********************************************************************
*/


/*!40101 SET NAMES utf8 */;

/*!40101 SET SQL_MODE=''*/;

/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test`;

/*Table structure for table `t2` */

DROP TABLE IF EXISTS `t2`;

CREATE TABLE `t2` (
  `id` int(11) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `t2` */

insert  into `t2`(`id`,`name`) values (1,'1'),(4,'4'),(7,'7'),(10,'10');

/*Table structure for table `teacher` */

DROP TABLE IF EXISTS `teacher`;

CREATE TABLE `teacher` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `age` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

/*Data for the table `teacher` */

insert  into `teacher`(`id`,`name`,`age`) values (1,'seven11124',18),(2,'qingshan',18);

/*Table structure for table `user_account` */

DROP TABLE IF EXISTS `user_account`;

CREATE TABLE `user_account` (
  `id` int(11) NOT NULL DEFAULT '0',
  `balance` int(11) NOT NULL,
  `lastUpdate` datetime NOT NULL,
  `userID` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

/*Data for the table `user_account` */

insert  into `user_account`(`id`,`balance`,`lastUpdate`,`userID`) values (1,3200,'2018-12-06 13:27:57',1),(2,50,'2018-12-06 13:28:08',2),(3,1000,'2018-12-06 13:28:22',3);

/*Table structure for table `users` */

DROP TABLE IF EXISTS `users`;

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) NOT NULL,
  `age` int(11) NOT NULL,
  `phoneNum` varchar(32) NOT NULL,
  `lastUpdate` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_eq_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4;

/*Data for the table `users` */

insert  into `users`(`id`,`name`,`age`,`phoneNum`,`lastUpdate`) values (1,'seven',26,'13666666666','2018-12-07 19:22:51'),(2,'qingshan',19,'13777777777','2018-12-08 21:01:12'),(3,'james',20,'13888888888','2018-12-08 20:59:39'),(4,'tom',99,'13444444444','2018-12-06 20:34:10'),(6,'jack',91,'13444444544','2018-12-06 20:35:07'),(11,'jack1',33,'13441444544','2018-12-06 20:36:19'),(15,'tom2',30,'1344444444','2018-12-08 15:08:24'),(19,'iiii',30,'1344444444','2018-12-08 21:21:47');

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

在運行下面的演示案例之前,先把表和數據準備好。

理解表鎖和行鎖

鎖是用於管理不同事務對共享資源的併發訪問。

表鎖與行鎖的區別:

  • 鎖定粒度:表鎖 > 行鎖
  • 加鎖效率:表鎖 > 行鎖
  • 衝突概率:表鎖 > 行鎖
  • 併發性能:表鎖 < 行鎖

InnoDB 存儲引擎支持行鎖和表鎖(另類的行鎖),InnoDB 的表鎖是通過對所有行加行鎖實現的。

鎖的類型

  • 共享鎖(行鎖):Shared Locks
  • 排他鎖(行鎖):Exclusive Locks
  • 意向鎖共享鎖(表鎖):Intention Shared Locks
  • 意向鎖排它鎖(表鎖):Intention Exclusive Locks
  • 自增鎖:AUTO-INC Locks

行鎖的算法

  • 記錄鎖:Record Locks
  • 間隙鎖:Gap Locks
  • 臨鍵鎖:Next-key Locks

官網文檔:

共享鎖(Shared Locks)

定義

共享鎖:又稱為讀鎖,簡稱 S 鎖,顧名思義,共享鎖就是多個事務對於同一數據可以共享一把鎖,都能訪問到數據,但是只能讀不能修改。

通過如下代碼,加鎖和釋放鎖:

-- 加鎖
select * from users WHERE id=1 LOCK IN SHARE MODE;

-- 釋放鎖:提交事務 or 回滾事務
commit;
rollback;

演示案例

-- 共享鎖
-- 事務A執行
BEGIN;

SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE;

ROLLBACK;
COMMIT;

-- 事務B執行
SELECT * FROM users WHERE id=1;

UPDATE users SET age=19 WHERE id=1;
  • 事務A手動開啟事務,執行語句獲取共享鎖,注意這裏沒有提交事務
  • 事務B分別執行 SELECT 和 UPDATE 語句,查看執行效果

結論:UPDATE 語句被鎖住了,不能執行。在事務A獲得共享鎖的情況下,事務B可以執行查詢操作,但是不能執行更新操作。

排他鎖(Exclusive Locks)

定義

排它鎖:又稱為寫鎖,簡稱 X 鎖,排他鎖不能與其他鎖並存,如一個事務獲取了一個數據行的排他鎖,其他事務就不能再獲取該行的鎖(共享鎖、排他鎖),只有該獲取了排他鎖的事務是可以對數據行進行讀取和修改。(其他事務要讀取數據可來自於快照)

通過如下代碼,加鎖和釋放鎖:

-- 加鎖
-- delete / update / insert 默認加上X鎖
-- SELECT * FROM table_name WHERE ... FOR UPDATE
-- 釋放鎖:提交事務 or 回滾事務
commit;
rollback;

演示案例

-- 排它鎖
-- 事務A執行
BEGIN;

UPDATE users SET age=23 WHERE id=1;

COMMIT;
ROLLBACK;

-- 事務B執行
SELECT * FROM users WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM users WHERE id=1 FOR UPDATE;
-- SELECT 可以執行,數據來自於快照
SELECT * FROM users WHERE id=1;
  • 事務A手動開啟事務,執行 UPDATE 語句,獲取排它鎖,注意這裏沒有提交事務
  • 事務B分別執行三條語句,查看執行效果

 

結論:事務B的第一條 SQL 和第二條 SQL 語句都不能執行,都已經被鎖住了,第三條 SQL 可以執行,數據來自於快照,關於這點後面會講到。

行鎖到底鎖了什麼

InnoDB 的行鎖是通過給索引上的索引項加鎖來實現的。

只有通過索引條件進行數據檢索,InnoDB 才使用行級鎖,否則,InnoDB 將使用表鎖(鎖住索引的所有記錄)

通過普通索引進行數據檢索,比如通過下面例子中 UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';該 SQL 會在 name 字段的唯一索引上面加一把行鎖,同時會在該唯一索引對應的主鍵索引上面也會加上一把行鎖,總共會加兩把行鎖。

演示案例

演示之前,先看一下 users 表的結構和數據內容。

-- 案例1
-- 事務A執行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13666666666';

ROLLBACK;

-- 事務B執行
UPDATE users SET lastUpdate=NOW() WHERE id=2;
UPDATE users SET lastUpdate=NOW() WHERE id=1;

-- 案例2
-- 事務A執行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE id=1;

ROLLBACK;

-- 事務B執行
UPDATE users SET lastUpdate=NOW() WHERE id=2;
UPDATE users SET lastUpdate=NOW() WHERE id=1;

-- 案例3
-- 事務A執行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';

ROLLBACK;

-- 事務B執行
UPDATE users SET lastUpdate=NOW() WHERE `name`='seven';
UPDATE users SET lastUpdate=NOW() WHERE id=1;
UPDATE users SET lastUpdate=NOW() WHERE `name`='qingshan';
UPDATE users SET lastUpdate=NOW() WHERE id=2;

注意:這裏演示的案例都是在事務A沒有提交之前,執行事務B的語句。

案例1執行結果如下圖所示:

案例2執行結果如下圖所示:

案例3執行結果如下圖所示:

意向共享鎖(Intention Shared Locks)& 意向排它鎖(Intention Exclusive Locks)

意向共享鎖(IS)

表示事務準備給數據行加入共享鎖,即一個數據行加共享鎖前必須先取得該表的 IS 鎖,意向共享鎖之間是可以相互兼容的。

意向排它鎖(IX)

表示事務準備給數據行加入排他鎖,即一個數據行加排他鎖前必須先取得該表的 IX 鎖,意向排它鎖之間是可以相互兼容的

意向鎖(IS 、IX)是 InnoDB 數據操作之前自動加的,不需要用戶干預。

意義:當事務想去進行鎖表時,可以先判斷意向鎖是否存在,存在時則可快速返回該表不能啟用表鎖。

演示案例

-- IS鎖的意義
-- 事務A執行
BEGIN;

UPDATE users SET lastUpdate=NOW() WHERE id=1;

ROLLBACK;

-- 事務B執行
-- 因為沒有通過索引條件進行數據檢索,所以這裏加的是表鎖
UPDATE users SET lastUpdate=NOW() WHERE phoneNum='13777777777';

結論:事務B的 SQL 因為沒有通過索引條件進行數據檢索,所以這裏加的是表鎖,在對錶加鎖之前會查看該表是否已經存在了意向鎖,因為事務A已經獲得了該表的意向鎖了,所以事務B不需要判斷每一行數據是否已經加鎖,可以快速通過意向鎖阻塞當前 SQL 的更新操作。

自增鎖(AUTO-INC Locks)

定義

針對自增列自增長的一個特殊的表級別鎖。

通過如下命令查看自增鎖的默認等級:

SHOW VARIABLES LIKE 'innodb_autoinc_lock_mode';

默認取值1,代表連續,事務未提交 ID 永久丟失。

演示案例

-- 事務A執行
BEGIN;
INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('tom2',30,'1344444444',NOW());
ROLLBACK;

BEGIN;
INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('xxx',30,'13444444444',NOW());
ROLLBACK;

-- 事務B執行
INSERT INTO users(NAME , age ,phoneNum ,lastUpdate ) VALUES ('yyy',30,'13444444444',NOW());

事務A執行完后,在執行事務B的語句,發現插入的 ID 數據不再連續,因為事務A獲取的 ID 數據在 ROLLBACK 之後被丟棄了。

臨鍵鎖(Next-Key Locks)

定義

當 SQL 執行按照索引進行數據的檢索時,查詢條件為範圍查找(between and、<、>等)並有數據命中,則此時 SQL 語句加上的鎖為 Next-key locks,鎖住索引的記錄 + 區間(左開右閉)。

演示案例

演示之前,先看一下 t2 表的結構和數據內容。

臨鍵鎖(Next-key Locks):InnoDB 默認的行鎖算法。

t2 表中的數據行有4條數據:1,4,7,10,InnoDB 引擎會將表中的數據劃分為:(-∞, 1] (1, 4] (4, 7] (7, 10] (10, +∞),執行如下 SQL 語句:

-- 臨鍵鎖
-- 事務A執行
BEGIN;

SELECT * FROM t2 WHERE id>5 AND id<9 FOR UPDATE;

ROLLBACK

-- 事務B執行
BEGIN;

SELECT * FROM t2 WHERE id=4 FOR UPDATE; -- 可以執行
SELECT * FROM t2 WHERE id=7 FOR UPDATE; -- 鎖住
SELECT * FROM t2 WHERE id=10 FOR UPDATE; -- 鎖住
INSERT INTO `t2` (`id`, `name`) VALUES (9, '9'); -- 鎖住

SELECT * FROM t2 WHERE id>5 AND id<9 FOR UPDATE; 這條查詢語句命中了7這條數據,它會鎖住 (4, 7] 這個區間,同時還會鎖住下一個區間 (7, 10]。

為什麼 InnoDB 選擇臨鍵鎖作為行鎖的默認算法?

防止幻讀。當我們把下一個區間也鎖住的時候,這個時候我們要新增數據,就會被鎖住,這樣就可以防止幻讀。

間隙鎖(Gap Locks)

定義

當 SQL 執行按照索引進行數據的檢索時,查詢條件的數據不存在,這時 SQL 語句加上的鎖即為 Gap locks,鎖住數據不存在的區間(左開右開)

Gap 只在 RR 事務隔離級別存在。因為幻讀問題是在 RR 事務通過臨鍵鎖和 MVCC 解決的,而臨鍵鎖=間隙鎖+記錄鎖,所以間隙鎖只在 RR 事務隔離級別存在。

演示案例

-- 間隙鎖
-- 事務A執行
BEGIN;

SELECT * FROM t2 WHERE id>4 AND id <6 FOR UPDATE;
-- 或者
SELECT * FROM t2 WHERE id=6 FOR UPDATE;

ROLLBACK;

-- 事務B執行
INSERT INTO `t2` (`id`, `name`) VALUES (5, '5');
INSERT INTO `t2` (`id`, `name`) VALUES (6, '6');

 SELECT * FROM t2 WHERE id>4 AND id <6 FOR UPDATE; 這條查詢語句不能命中數據,它會鎖住 (4, 7] 這個區間。

記錄鎖(Record Locks)

定義

當 SQL 執行按照唯一性(Primary key、Unique key)索引進行數據的檢索時,查詢條件等值匹配且查詢的數據是存在,這時 SQL 語句加上的鎖即為記錄鎖 Record Locks,鎖住具體的索引項。

演示案例

-- 記錄鎖
-- 事務A執行
BEGIN;

SELECT * FROM t2 WHERE id=4 FOR UPDATE;

ROLLBACK;


-- 事務B執行
SELECT * FROM t2 WHERE id=7 FOR UPDATE;
SELECT * FROM t2 WHERE id=4 FOR UPDATE;

事務A執行 SELECT * FROM t2 WHERE id=4 FOR UPDATE; 把 id=4 的數據行鎖住。

當 SQL 執行按照普通索引進行數據的檢索時,查詢條件等值匹配且查詢的數據是存在,這時 SQL 語句鎖住數據存在區間(左開右開)

利用鎖解決事務併發帶來的問題

InnoDB 真正處理事務併發帶來的問題不僅僅是依賴鎖,還有其他的機制,下篇文章會講到,所以這裏只是演示利用鎖是如何解決事務併發帶來的問題,並不是 InnoDB 真實的處理方式。

利用鎖怎麼解決臟讀

在事務B的更新語句上面加上一把 X 鎖,這樣就可以有效的解決臟讀問題。

利用鎖怎麼解決不可重複讀

在事務A的查詢語句上面加上一把 S 鎖,事務B的更新操作將會被阻塞,這樣就可以有效的解決不可重複讀的問題。

利用鎖怎麼解決幻讀

在事務A的查詢語句上面加上一把 Next-key 鎖,通過臨鍵鎖的定義,可以知道這個時候,事務A會把 (-∞,+∞) 的區間數據都鎖住,事務B的新增操作將會被阻塞,這樣就可以有效的解決幻讀的問題。

死鎖

死鎖的介紹

  • 多個併發事務(2個或者以上);
  • 每個事務都持有鎖(或者是已經在等待鎖);
  • 每個事務都需要再繼續持有鎖;
  • 事務之間產生加鎖的循環等待,形成死鎖。

演示案例

-- 事務A執行
BEGIN;

UPDATE users SET lastUpdate = NOW() WHERE id =1;

UPDATE t2 SET `name`='test' WHERE id =1;

ROLLBACK;

-- 事務B執行
BEGIN;

UPDATE t2 SET `name`='test' WHERE id =1;

UPDATE users SET lastUpdate = NOW() WHERE id =1;

ROLLBACK;

事務A和事務B按照上面的執行步驟,最後因為存在相互等待的情況,所以 MySQL 判斷出現死鎖了。

死鎖的避免

  • 類似的業務邏輯以固定的順序訪問表和行。
  • 大事務拆小。大事務更傾向於死鎖,如果業務允許,將大事務拆小。
  • 在同一個事務中,盡可能做到一次鎖定所需要的所有資源,減少死鎖概率。
  • 降低隔離級別,如果業務允許,將隔離級別調低也是較好的選擇
  • 為表添加合理的索引。可以看到如果不走索引將會為表的每一行記錄添加上鎖(或者說是表鎖)

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

【其他文章推薦】

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

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

※大陸寄台灣空運注意事項

※大陸海運台灣交貨時間多久?

ThreadLocal 源碼解析

在activeJDBC框架內部的實現中看到了 ThreadLocal 這個類,記錄下了每個線程獨有的連接

private static final ThreadLocal<HashMap<String, Connection>> connectionsTL = new ThreadLocal<>();

感覺是個知識點,就打開源碼看看了。先看一下源碼里的解釋

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

這個鳥文,瞎翻譯一下,就是:

這個類提供了供線程專享的變量。這些變量不同與其它普通的變量,它是每個線程都有一個自己的獨立初始化的變量(通過get和set方法實現)。這個類的實例常用於類的私有靜態字段,以實現每個線程都有自己的狀態(例如userId,事務ID等)。

先跑一下用法吧,

package com.test.threadlocal;

public class TestController {
    
    private static int index = 0;
    private static String str = "這個字符串是每個線程共享的";
    // 這個變量,看似是一個類的靜態屬性,實則是每個線程有自己獨有的區域
    private static ThreadLocal<String> threadStr = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return "main線程專享";
        }
    };      
    
    public static void main(String[] args) throws InterruptedException {
        for(int i = 0; i < 3; i++) {
            Thread t = new MyThread();
            t.start();
            t.join();
        }
        
        System.out.println(str);
        System.out.println(threadStr.get());
    }
    
    static class MyThread extends Thread{

        @Override
        public void run() {
            index++;
            str = "第" + index + "個str";
            threadStr.set("第" + index + "個threadStr");
        }
        
        
    }

}

這個例子中,從str和threadStr變量的打印結果可以看出來。str被所有的線程讀和寫,threadStr在每個線程內部開闢了一塊線程專享的區域。接下來,我們看一下具體實現。
先看一下構造函數

     /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();
     /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

構造函數是空的,但是,該類有一個私有整型常量threadLocalHashCode。nextHashCode()方法我們就不看了,省的一如源碼深似海。看鳥文的話,大概就是每new一個ThreadLocal變量的時候,就會生成一個散列碼,該碼非極端情況下與某個整數取模后不容易衝突(這句話有點迷吧,其實我也不懂)
然後看一下set方法

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

容易看出,這個方法設置每個線程自己的value,相當於當前線程是key,然後得出一個ThreadLocalMap。顯然,這個map用來保存線程內部的值,既然是map當然每個線程可以保存多個數值了,該map的value我們猜一下就是我要保存的具體的值,估計是用Object類聲明的。那key是什麼呢?我們看下ThreadLocalMap類的構造方法。

/**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
                
/**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

我的天!這個類沒有繼承我們想象中的HashMap,或者是ConcurrentMap,但是看過,Map的內部實現的同學應該可以發現,這個Map的實現和HashMap的實現簡直就是小巫見大巫,有沒有。它在構造函數中做了如下幾步:

  1. 初始化一個大小為16的Entry數組
  2. 通過上面說過的很迷的HashCode散列值與15取模得到將要存儲在數組中的索引值
  3. 構造Entry,然後保存進去
  4. 長度設置為1
  5. 設置要擴容的限制大小為16的2/3

我們看到這個不就是用數組實現的Map嘛,看過HashMap實現的我們,覺得洒洒水啦。
Map的set和get方法就不分析了。ThreadLocal的get方法我們還是要貼出來的,畢竟是我們主要分析的東西

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

可見,是獲取到當前線程,用作key獲取到Map,然後用當前this獲取到Entry實體。最後當然獲取到了存儲的value。

我編碼,我快樂~

本文由博客一文多發平台 發布!

我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=19l4zaaz7g6ct

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

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

從EFCore上下文的使用到深入剖析DI的生命周期最後實現自動屬性注入

故事背景

最近在把自己的一個老項目從Framework遷移到.Net Core 3.0,數據訪問這塊選擇的是EFCore+Mysql。使用EF的話不可避免要和DbContext打交道,在Core中的常規用法一般是:創建一個XXXContext類繼承自DbContext,實現一個擁有DbContextOptions參數的構造器,在啟動類StartUp中的ConfigureServices方法里調用IServiceCollection的擴展方法AddDbContext,把上下文注入到DI容器中,然後在使用的地方通過構造函數的參數獲取實例。OK,沒任何毛病,官方示例也都是這麼來用的。但是,通過構造函數這種方式來獲取上下文實例其實很不方便,比如在Attribute或者靜態類中,又或者是系統啟動時初始化一些數據,更多的是如下一種場景:

    public class BaseController : Controller
    {
        public BloggingContext _dbContext;
        public BaseController(BloggingContext dbContext)
        {
            _dbContext = dbContext;
        }

        public bool BlogExist(int id)
        {
            return _dbContext.Blogs.Any(x => x.BlogId == id);
        }
    }

    public class BlogsController : BaseController
    {
        public BlogsController(BloggingContext dbContext) : base(dbContext) { }
    }

從上面的代碼可以看到,任何要繼承BaseController的類都要寫一個“多餘”的構造函數,如果參數再多幾個,這將是無法忍受的(就算只有一個參數我也忍受不了)。那麼怎樣才能更優雅的獲取數據庫上下文實例呢,我想到以下幾種辦法。


DbContext從哪來

1、  直接開溜new

回歸原始,既然要創建實例,沒有比直接new一個更好的辦法了,在Framework中沒有DI的時候也差不多都這麼干。但在EFCore中不同的是,DbContext不再提供無參構造函數,取而代之的是必須傳入一個DbContextOptions類型的參數,這個參數通常是做一些上下文選項配置例如使用什麼類型數據庫連接字符串是多少。

        public BloggingContext(DbContextOptions<BloggingContext> options) : base(options)
        {
        }

默認情況下,我們已經在StartUp中註冊上下文的時候做了配置,DI容器會自動幫我們把options傳進來。如果要手動new一個上下文,那豈不是每次都要自己傳?不行,這太痛苦了。那有沒有辦法不傳這個參數?肯定也是有的。我們可以去掉有參構造函數,然後重寫DbContext中的OnConfiguring方法,在這個方法中做數據庫配置: 

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlite("Filename=./efcoredemo.db");
        }

即使是這樣,依然有不夠優雅的地方,那就是連接字符串被硬編碼在代碼中,不能做到從配置文件讀取。反正我忍受不了,只能再尋找其他方案。

2、  從DI容器手動獲取

既然前面已經在啟動類中註冊了上下文,那麼從DI容器中獲取實例肯定是沒問題的。於是我寫了這樣一句測試代碼用來驗證猜想:

    var context = app.ApplicationServices.GetService<BloggingContext>();

不過很遺憾拋出了異常:

報錯信息說的很明確,不能從root provider中獲取這個服務。我從G站下載了DI框架的源碼(地址是),拿報錯信息進行反向追溯,發現異常來自於CallSiteValidator類的ValidateResolution方法:

        public void ValidateResolution(Type serviceType, IServiceScope scope, IServiceScope rootScope)
        {
            if (ReferenceEquals(scope, rootScope)
                && _scopedServices.TryGetValue(serviceType, out var scopedService))
            {
                if (serviceType == scopedService)
                {
                    throw new InvalidOperationException(
                        Resources.FormatDirectScopedResolvedFromRootException(serviceType,
                            nameof(ServiceLifetime.Scoped).ToLowerInvariant()));
                }

                throw new InvalidOperationException(
                    Resources.FormatScopedResolvedFromRootException(
                        serviceType,
                        scopedService,
                        nameof(ServiceLifetime.Scoped).ToLowerInvariant()));
            }
        }

View Code

繼續往上,看到了GetService方法的實現:

        internal object GetService(Type serviceType, ServiceProviderEngineScope serviceProviderEngineScope)
        {
            if (_disposed)
            {
                ThrowHelper.ThrowObjectDisposedException();
            }

            var realizedService = RealizedServices.GetOrAdd(serviceType, _createServiceAccessor);
            _callback?.OnResolve(serviceType, serviceProviderEngineScope);
            DependencyInjectionEventSource.Log.ServiceResolved(serviceType);
            return realizedService.Invoke(serviceProviderEngineScope);
        }

View Code

可以看到,_callback在為空的情況下是不會做驗證的,於是猜想有參數能對它進行配置。把追溯對象換成_callback繼續往上翻,在DI框架的核心類ServiceProvider中找到如下方法:

        internal ServiceProvider(IEnumerable<ServiceDescriptor> serviceDescriptors, ServiceProviderOptions options)
        {
            IServiceProviderEngineCallback callback = null;
            if (options.ValidateScopes)
            {
                callback = this;
                _callSiteValidator = new CallSiteValidator();
            }
            //省略....
        }    

說明我的猜想沒錯,驗證是受ValidateScopes控制的。這樣來看,把ValidateScopes設置成False就可以解決了,這也是網上普遍的解決方案:

      .UseDefaultServiceProvider(options =>
       {
              options.ValidateScopes = false;
       })

但這樣做是極其危險的。

為什麼危險?到底什麼是root provider?那就要從原生DI的生命周期說起。我們知道,DI容器被封裝成一個IServiceProvider對象,服務都是從這裏來獲取。不過這並不是一個單一對象,它是具有層級結構的,最頂層的即前面提到的root provider,可以理解為僅屬於系統層面的DI控制中心。在Asp.Net Core中,內置的DI有3種服務模式,分別是Singleton、Transient、Scoped,Singleton服務實例是保存在root provider中的,所以它才能做到全局單例。相對應的Scoped,是保存在某一個provider中的,它能保證在這個provider中是單例的,而Transient服務則是隨時需要隨時創建,用完就丟棄。由此可知,除非是在root provider中獲取一個單例服務,否則必須要指定一個服務範圍(Scope),這個驗證是通過ServiceProviderOptions的ValidateScopes來控制的。默認情況下,Asp.Net Core框架在創建HostBuilder的時候會判定當前是否開發環境,在開發環境下會開啟這個驗證:

所以前面那種關閉驗證的方式是錯誤的。這是因為,root provider只有一個,如果恰好有某個singleton服務引用了一個scope服務,這會導致這個scope服務也變成singleton,仔細看一下註冊DbContext的擴展方法,它實際上提供的是scope服務:

如果發生這種情況,數據庫連接會一直得不到釋放,至於有什麼後果大家應該都明白。

所以前面的測試代碼應該這樣寫:

     using (var serviceScope = app.ApplicationServices.CreateScope())
     {
         var context = serviceScope.ServiceProvider.GetService<BloggingContext>();
     }

與之相關的還有一個ValidateOnBuild屬性,也就是說在構建IServiceProvider的時候就會做驗證,從源碼中也能體現出來:

            if (options.ValidateOnBuild)
            {
                List<Exception> exceptions = null;
                foreach (var serviceDescriptor in serviceDescriptors)
                {
                    try
                    {
                        _engine.ValidateService(serviceDescriptor);
                    }
                    catch (Exception e)
                    {
                        exceptions = exceptions ?? new List<Exception>();
                        exceptions.Add(e);
                    }
                }

                if (exceptions != null)
                {
                    throw new AggregateException("Some services are not able to be constructed", exceptions.ToArray());
                }
            }

View Code

正因為如此,Asp.Net Core在設計的時候為每個請求創建獨立的Scope,這個Scope的provider被封裝在HttpContext.RequestServices中。

 [小插曲]

通過代碼提示可以看到,IServiceProvider提供了2種獲取service的方式:

這2個有什麼區別呢?分別查看各自的方法摘要可以看到,通過GetService獲取一個沒有註冊的服務時會返回null,而GetRequiredService會拋出一個InvalidOperationException,僅此而已。

        // 返回結果:
        //     A service object of type T or null if there is no such service.
        public static T GetService<T>(this IServiceProvider provider);

        // 返回結果:
        //     A service object of type T.
        //
        // 異常:
        //   T:System.InvalidOperationException:
        //     There is no service of type T.
        public static T GetRequiredService<T>(this IServiceProvider provider);

 

終極大招

到現在為止,儘管找到了一種看起來合理的方案,但還是不夠優雅,使用過其他第三方DI框架的朋友應該知道,屬性注入的快感無可比擬。那原生DI有沒有實現這個功能呢,我滿心歡喜上G站搜Issue,看到這樣一個回復():

官方明確表示沒有開發屬性注入的計劃,沒辦法,只能靠自己了。

我的思路大概是:創建一個自定義標籤(Attribute),用來給需要注入的屬性打標籤,然後寫一個服務激活類,用來解析給定實例需要注入的屬性並賦值,在某個類型被創建實例的時候也就是構造函數中調用這個激活方法實現屬性注入。這裡有個核心點要注意的是,從DI容器獲取實例的時候一定要保證是和當前請求是同一個Scope,也就是說,必須要從當前的HttpContext中拿到這個IServiceProvider。

先創建一個自定義標籤:

    [AttributeUsage(AttributeTargets.Property)]
    public class AutowiredAttribute : Attribute
    {

    }

解析屬性的方法:

        public void PropertyActivate(object service, IServiceProvider provider)
        {
            var serviceType = service.GetType();
            var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_"));
            foreach (PropertyInfo property in properties)
            {
                var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
                if (autowiredAttr != null)
                {
                    //從DI容器獲取實例
                    var innerService = provider.GetService(property.PropertyType);
                    if (innerService != null)
                    {
                        //遞歸解決服務嵌套問題
                        PropertyActivate(innerService, provider);
                        //屬性賦值
                        property.SetValue(service, innerService);
                    }
                }
            }
        }

然後在控制器中激活屬性:

        [Autowired]
        public IAccountService _accountService { get; set; }

        public LoginController(IHttpContextAccessor httpContextAccessor)
        {
            var pro = new AutowiredServiceProvider();
            pro.PropertyActivate(this, httpContextAccessor.HttpContext.RequestServices);
        }

這樣子下來,雖然功能實現了,但是裏面存着幾個問題。第一個是由於控制器的構造函數中不能直接使用ControllerBase的HttpContext屬性,所以必須要通過注入IHttpContextAccessor對象來獲取,貌似問題又回到原點。第二個是每個構造函數中都要寫這麼一堆代碼,不能忍。於是想有沒有辦法在控制器被激活的時候做一些操作?沒考慮引入AOP框架,感覺為了這一個功能引入AOP有點重。經過網上搜索,發現Asp.Net Core框架激活控制器是通過IControllerActivator接口實現的,它的默認實現是DefaultControllerActivator():

       /// <inheritdoc />
        public object Create(ControllerContext controllerContext)
        {
            if (controllerContext == null)
            {
                throw new ArgumentNullException(nameof(controllerContext));
            }

            if (controllerContext.ActionDescriptor == null)
            {
                throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(ControllerContext.ActionDescriptor),
                    nameof(ControllerContext)));
            }

            var controllerTypeInfo = controllerContext.ActionDescriptor.ControllerTypeInfo;

            if (controllerTypeInfo == null)
            {
                throw new ArgumentException(Resources.FormatPropertyOfTypeCannotBeNull(
                    nameof(controllerContext.ActionDescriptor.ControllerTypeInfo),
                    nameof(ControllerContext.ActionDescriptor)));
            }

            var serviceProvider = controllerContext.HttpContext.RequestServices;
            return _typeActivatorCache.CreateInstance<object>(serviceProvider, controllerTypeInfo.AsType());
        }

View Code

這樣一來,我自己實現一個Controller激活器不就可以接管控制器激活了,於是有如下這個類:

    public class HosControllerActivator : IControllerActivator
    {
        public object Create(ControllerContext actionContext)
        {
            var controllerType = actionContext.ActionDescriptor.ControllerTypeInfo.AsType();
            var instance = actionContext.HttpContext.RequestServices.GetRequiredService(controllerType);
            PropertyActivate(instance, actionContext.HttpContext.RequestServices);
            return instance;
        }

        public virtual void Release(ControllerContext context, object controller)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }
            if (controller == null)
            {
                throw new ArgumentNullException(nameof(controller));
            }
            if (controller is IDisposable disposable)
            {
                disposable.Dispose();
            }
        }

        private void PropertyActivate(object service, IServiceProvider provider)
        {
            var serviceType = service.GetType();
            var properties = serviceType.GetProperties().AsEnumerable().Where(x => x.Name.StartsWith("_"));
            foreach (PropertyInfo property in properties)
            {
                var autowiredAttr = property.GetCustomAttribute<AutowiredAttribute>();
                if (autowiredAttr != null)
                {
                    //從DI容器獲取實例
                    var innerService = provider.GetService(property.PropertyType);
                    if (innerService != null)
                    {
                        //遞歸解決服務嵌套問題
                        PropertyActivate(innerService, provider);
                        //屬性賦值
                        property.SetValue(service, innerService);
                    }
                }
            }
        }
    }

View Code

需要注意的是,DefaultControllerActivator中的控制器實例是從TypeActivatorCache獲取的,而自己的激活器是從DI獲取的,所以必須額外把系統所有控制器註冊到DI中,封裝成如下的擴展方法:

        /// <summary>
        /// 自定義控制器激活,並手動註冊所有控制器
        /// </summary>
        /// <param name="services"></param>
        /// <param name="obj"></param>
        public static void AddHosControllers(this IServiceCollection services, object obj)
        {
            services.Replace(ServiceDescriptor.Transient<IControllerActivator, HosControllerActivator>());
            var assembly = obj.GetType().GetTypeInfo().Assembly;
            var manager = new ApplicationPartManager();
            manager.ApplicationParts.Add(new AssemblyPart(assembly));
            manager.FeatureProviders.Add(new ControllerFeatureProvider());
            var feature = new ControllerFeature();
            manager.PopulateFeature(feature);
            feature.Controllers.Select(ti => ti.AsType()).ToList().ForEach(t =>
            {
                services.AddTransient(t);
            });
        }

View Code

在ConfigureServices中調用:

services.AddHosControllers(this);

到此,大功告成!可以愉快的繼續CRUD了。

 

結尾

市面上好用的DI框架一堆一堆的,集成到Core裏面也很簡單,為啥還要這麼折騰?沒辦法,這不就是造輪子的樂趣嘛。上面這些東西從頭到尾也折騰了不少時間,屬性注入那裡也還有優化的空間,歡迎探討。

推薦閱讀:

 

 

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理
【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?

本田計畫到2030年新能源汽車比例將達到三分之二

據《財富》網路版報導,本田週三表示,計畫把新能源汽車比例由當前的5%提高到2030年的三分之二。

本田在發佈的最新戰略中稱,在未來不到15年,油田混合動力、插電式混合動力、電池動力和燃料電池動力汽車產量將超過燃油動力汽車。

本田首席執行官八鄉隆弘(Takahiro Hachigo)說,插電式混合動力汽車將是公司“未來電氣化的核心”。

八鄉隆弘說,本田將於2018年在北美推出一款插電式混合動力汽車,然後會推出主要車型的插電式版本,並增加車型數量。
 

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

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

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

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

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

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

※試算大陸海運運費!

工信部:投入5億元組建動力電池研發平臺

工信部部長苗圩25日表示,工信部已聯合行業內外的九家企業投入5億元資本金,正組建動力電池研究院,或者動力電池的研究研發平臺。

日前召開的國務院常務會議提出,要加快實現動力電池革命性突破。中國汽車工業協會常務副會長董揚認為,這是“補短板”之舉,“對中國來說,要逐步達到世界平均水準,需要材料、裝備、生產、市場等各方協同發展”。

據瞭解,目前我國新能源汽車累計產量接近50萬輛,但車用動力電池與一些發達國家有相隔一代的差距。除比亞迪外,其他生產新能源汽車的企業都沒有電池研發。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

※網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

※想要讓你的商品成為最夯、最多人討論的話題?網頁設計公司讓你強力曝光

※想知道最厲害的台北網頁設計公司推薦、台中網頁設計公司推薦專業設計師”嚨底家”!!

※專營大陸快遞台灣服務

※台灣快遞大陸的貨運公司有哪些呢?