pythonic context manager知多少

Context Managers 是我最喜歡的 python feature 之一,在恰當的時機使用 context manager 使代碼更加簡潔、清晰,更加安全,復用性更好,更加 pythonic。本文簡單介紹一下其使用方法以及常見使用場景。

本文地址:https://www.cnblogs.com/xybaby/p/13202496.html

with statement and context manager

Python’s with statement supports the concept of a runtime context defined by a context manager

new statement “with” to the Python language to make it possible to factor out standard uses of try/finally statements.

在 pep0343 中,通過引入 context manager protocol 來支持 With statement , context manager 是用來管理 context(上下文)的,即保證程序要保持一種特定的狀態 — 無論是否發生異常。可以說,context manager 簡化了對 try-finally 的使用,而且更加安全,更加便於使用。

Transforming Code into Beautiful, Idiomatic Python 中,指出了 context manager 的最顯著的優點:

  • Helps separate business logic from administrative logic
  • Clean, beautiful tools for factoring code and improving code reuse

最廣為人知的例子,就是通過 with statement 來讀寫文件,代碼如下:

with open('test.txt') as f:
    contect = f.read()
    handle_content(content)

上面的代碼幾乎等價於

f = open('test.txt') 
try:
    contect = f.read()
    handle_content(content)
finally:
    f.close()

注意,上面的finally的作用就是保證file.close一定會被調用,也就是資源一定會釋放。不過,很多時候,都會忘了去寫這個finally,而 with statement 就徹底避免了這個問題。

從上述兩段代碼也可以看出,with statement 更加簡潔,而且將核心的業務邏輯(從文件中讀取、處理數據)與其他邏輯(打開、關係文件)相分離,可讀性更強。

實現context manager protocol

一個類只要定義了__enter____exit__方法就實現了context manager 協議

object.__enter__(self)
Enter the runtime context related to this object. The with statement will bind this method’s return value to the target(s) specified in the as clause of the statement, if any.

object.__exit__(self, exc_type, exc_value, traceback)
Exit the runtime context related to this object. The parameters describe the exception that caused the context to be exited. If the context was exited without an exception, all three arguments will be None.

If an exception is supplied, and the method wishes to suppress the exception (i.e., prevent it from being propagated), it should return a true value. Otherwise, the exception will be processed normally upon exit from this method.

Note that __exit__() methods should not reraise the passed-in exception; this is the caller’s responsibility.

__enter__方法在進入這個 context 的時候調用,返回值賦值給 with as X 中的 X

__exit__方法在退出 context 的時候調用,如果沒有異常,后三個參數為 None。如果返回值為 True,則Suppress Exception,所以除非特殊情況都應返回 False。另外注意, __exit__方法本身不應該拋出異常。

例子:BlockGuard

在看c++代碼(如mongodb源碼)的時候,經常看見其用 RAII 實現BlockGuard, 用以保證在離開 Block 的時候執行某些動作,同時,也提供手段來取消執行。

下面用python實現一下:

class BlockGuard(object):
	def __init__(self, fn, *args, **kwargs):
		self._fn = fn
		self._args = args
		self._kwargs = kwargs
		self._canceled = False

	def __enter__(self):
		return self

	def __exit__(self, exc_type, exc_value, traceback):
		if not self._canceled:
			self._fn(*self._args, **self._kwargs)
		self._fn = None
		self._args = None
		self._kwargs = None
		return False

	def cancel(self):
		self._canceled = True


def foo():
	print 'sth should be called'


def test_BlockGuard(cancel_guard):
	print 'test_BlockGuard'
	with BlockGuard(foo) as guard:
		if cancel_guard:
			guard.cancel()
	print 'test_BlockGuard  finish'

用yield實現context manager

標準庫 contextlib 中提供了一些方法,能夠簡化我們使用 context manager,如 contextlib.contextmanager(func) 使我們
無需再去實現一個包含__enter__ __exit__方法的類。

The function being decorated must return a generator-iterator when called. This iterator must yield exactly one value, which will be bound to the targets in the with statement’s as clause, if any.

例子如下:

from contextlib import contextmanager

@contextmanager
def managed_resource(*args, **kwds):
    # Code to acquire resource, e.g.:
    resource = acquire_resource(*args, **kwds)
    try:
        yield resource
    finally:
        # Code to release resource, e.g.:
        release_resource(resource)

>>> with managed_resource(timeout=3600) as resource:
...     # Resource is released at the end of this block,
...     # even if code in the block raises an exception

需要注意的是:

  • 一定要寫 try finally,才能保證release_resource邏輯一定被調用
  • 除非特殊情況,不再 catch exception,這就跟 __exit__ 一般不返回True一樣

例子: no_throw

這是業務開發中的一個需求, 比如觀察者模式,不希望因為其中一個觀察者出了 trace 就影響後續的觀察者,就可以這樣做:

from contextlib import contextmanager

@contextmanager
def no_throw(*exceptions):
	try:
		yield
	except exceptions:
		pass

def notify_observers(seq):
	for fn in [sum, len, max, min]:
		with no_throw(Exception):
			print "%s result %s" % (fn.__name__, fn(seq))

if __name__ == '__main__':
	notify_observers([])

在python 3.x 的 contexlib 中,就提供了一個contextlib.suppress(*exceptions), 實現了同樣的效果。

context manager 應用場景

context manager 誕生的初衷就在於簡化 try-finally,因此就適合應用於在需要 finally 的地方,也就是需要清理的地方,比如

  • 保證資源的安全釋放,如 file、lock、semaphore、network connection 等
  • 臨時操作的復原,如果一段邏輯有 setup、prepare,那麼就會對應 cleanup、teardown。

對於第一種情況,網絡連接釋放的例子,後面會結合 pymongo 的代碼展示。

在這裏先來看看第二種用途:保證代碼在一個臨時的、特殊的上下文(context)中執行,且在執行結束之後恢復到之前的上下文環境。

改變工作目錄

from contextlib import contextmanager
import os

@contextmanager
def working_directory(path):
    current_dir = os.getcwd()
    os.chdir(path)
    try:
        yield
    finally:
        os.chdir(current_dir)

with working_directory("data/stuff"):
    pass

臨時文件、文件夾

很多時候會產生一堆臨時文件,比如build的中間狀態,這些臨時文件都需要在結束之後清除。

from tempfile import mkdtemp
from shutil import rmtree

@contextmanager
def temporary_dir(*args, **kwds):
    name = mkdtemp(*args, **kwds)
    try:
        yield name
    finally:
        shutil.rmtree(name)

with temporary_dir() as dirname:
    pass

重定向標準輸出、標準錯誤

@contextmanager
def redirect_stdout(fileobj):
    oldstdout = sys.stdout
    sys.stdout = fileobj
    try:
        yield fieldobj
    finally:
        sys.stdout = oldstdout

在 python3.x 中,已經提供了 contextlib.redirect_stdout contextlib.redirect_stderr 實現上述功能

調整logging level

這個在查問題的適合非常有用,一般生產環境不會輸出 debug level 的日誌,但如果出了問題,可以臨時對某些制定的函數調用輸出debug 日誌

from contextlib import contextmanager
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logger.addHandler(ch)


@contextmanager
def change_log_level(level):
	old_level = logger.getEffectiveLevel()
	try:
		logger.setLevel(level)
		yield
	finally:
		logger.setLevel(old_level)


def test_logging():
	logger.debug("this is a debug message")
	logger.info("this is a info message")
	logger.warn("this is a warning message")

with change_log_level(logging.DEBUG):
	test_logging()

pymongo中的context manager使用

在 pymongo 中,封裝了好幾個 context manager,用以

  • 管理 semaphore
  • 管理 connection
  • 資源清理

而且,在 pymongo 中,給出了嵌套使用 context manager 的好例子,用來保證 socket 在使用完之後一定返回連接池(pool)。

# server.py
@contextlib.contextmanager
def get_socket(self, all_credentials, checkout=False):
    with self.pool.get_socket(all_credentials, checkout) as sock_info:
        yield sock_info
        
# pool.py
@contextlib.contextmanager
def get_socket(self, all_credentials, checkout=False):
    sock_info = self._get_socket_no_auth()
    try:
        sock_info.check_auth(all_credentials)
        yield sock_info
    except:
        # Exception in caller. Decrement semaphore.
        self.return_socket(sock_info)
        raise
    else:
        if not checkout:
            self.return_socket(sock_info)

可以看到,server.get_socket 調用了 pool.get_socket, 使用 server.get_socket 的代碼完全不了解、也完全不用關心 socket 的釋放細節,如果把 try-except-finally-else 的邏輯移到所有使用socket的地方,代碼就會很醜、很臃腫。

比如,在mongo_client 中需要使用到 socket:

with server.get_socket(all_credentials) as sock_info:
    sock_info.authenticate(credentials)

references

With statement

Context Managers

contextlib

what-is-the-python-with-statement-designed-for

Transforming Code into Beautiful, Idiomatic Python

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

※幫你省時又省力,新北清潔一流服務好口碑

上周熱點回顧(6.22-6.28)

熱點隨筆:

· 程序員敲代碼時耳機里聽的到底是什麼? (風的姿態)
· CPU明明8個核,網卡為啥拚命折騰一號核? (軒轅之風)
· 手把手教你基於SqlSugar4編寫一個可視化代碼生成器(生成實體,以SqlServer為例,文末附源碼) (熊澤-學習中的苦與樂)
· 在運行時生成C# .NET類 (芝麻麻雀)
· 因為我的一個低級錯誤,生產數據庫崩潰了將近半個小時 (鄙人薛某)
· C# 人臉識別庫 (View12138)
· 基於領域驅動設計(DDD)超輕量級快速開發架構 (阿新)
· .Net Core 中GC的工作原理 (她微笑的臉)
· 關於技術文章“標題黨”一事我想說兩句 (精緻碼農)
· 【故障公告】阿里雲 RDS 實例 CPU 100% 故障引發全站無法正常訪問 (博客園團隊)
· 思考:如何保證服務穩定性? (老_張)
· 只看到了別人28歲從字節跳動退休,背後的期權知識你知道嗎? (四猿外)

熱點新聞:

· 瘋王,任正非!
· VSCode彩虹屁插件:釘宮理惠,英雄聯盟版現已生成,你Pick哪一個?
· 全國首創!廣東人坐火車就像坐地鐵一樣方便了:無需提前買票
· 95后快遞小哥獲評“高層次人才”:杭州買房享受百萬元補貼
· 二線手機廠商墜落簡史:鎚子、魅族、金立已成過客
· 外賣員確診背後:年近50 每天接老婆下班 工作14小時
· 歷時26年!中國終於有了自己的全球導航系統
· 看!北斗三號最後一顆組網衛星在太空張開“翅膀” 畫面燃了
· 攻克地獄級難度!川藏鐵路拉林段120座橋樑主體工程全部完工
· 作家王小山控訴攜程欠錢不還:願意下跪懇請梁建章退租
· 知乎熱議:替代Matlab的國產軟件出現 半年內實現Matlab功能的70%
· 這個比QQ空間還古老的網站 是多少女孩的精神家園?

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

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

重學 Java 設計模式:實戰備忘錄模式「模擬互聯網系統上線過程中,配置文件回滾場景」

作者:小傅哥
博客:https://bugstack.cn – 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

實現不了是研發的借口?

實現不了,有時候是功能複雜度較高難以實現,有時候是工期較短實現不完。而編碼的行為又是一個不太好量化的過程,同樣一個功能每個人的實現方式不一樣,遇到開發問題解決問題的速度也不一樣。除此之外還很不好給產品解釋具體為什麼要這個工期時間,這就像蓋樓的圖紙最終要多少水泥砂漿一樣。那麼這時研發會盡可能的去通過一些經驗,制定流程規範、設計、開發、評審等,確定一個可以完成的時間範圍,又避免風險的時間點后。再被壓縮,往往會出一些矛盾點,能壓縮要解釋為什麼之前要那麼多時間,不能壓縮又有各方不斷施加的壓力。因此有時候不一定是借口,是要考慮如何讓整個團隊健康的發展。

鼓勵有時比壓力要重要!

在學習的過程中,很多時候我們聽到的都是,你要怎樣,怎樣,你瞧瞧誰誰誰,哪怕今天聽不到這樣的聲音了,但因為曾經反覆聽到過而導致內心抗拒。雖然也知道自己要去學,但是很難堅持,學着學着就沒有了方向,看到還有那麼多不會的就更慌了,以至於最後心態崩了,更不願意學。其實程序員的壓力並不小,想成長几乎是需要一直的學習,就像似乎再也不敢說精通java了一樣,知識量實在是隨着學習的深入,越來越深,越來越廣。所以需要,開心學習,快樂成長!

臨陣的你好像一直很着急!

經常的聽到;老師明天就要了你幫我弄弄吧你給我寫一下完事我就學這次着急現在這不是沒時間學嗎快給我看看。其實看到的類似的還有很多,很納悶你的着急怎麼來的,不太可能,人在家中坐,禍從天上落。老師怎麼就那個時間找你了,老闆怎麼就今天管你要了,還不是日積月累你沒有學習,臨時抱佛腳亂着急!即使後來真的有人幫你了,但最好不要放鬆,要儘快學會,躲得過初一還有初二呢!

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-17-00 開發配置文件備忘錄

三、備忘錄模式介紹

備忘錄模式是以可以恢復或者說回滾,配置、版本、悔棋為核心功能的設計模式,而這種設計模式屬於行為模式。在功能實現上是以不破壞原對象為基礎增加備忘錄操作類,記錄原對象的行為從而實現備忘錄模式。

這個設計在我們平常的生活或者開發中也是比較常見的,比如:後悔葯、孟婆湯(一下回滾到0),IDEA編輯和撤銷、小霸王遊戲機存檔。當然還有我們非常常見的Photoshop,如下;

四、案例場景模擬

在本案例中我們模擬系統在發布上線的過程中記錄線上配置文件用於緊急回滾

在大型互聯網公司系統的發布上線一定是易用、安全、可處理緊急狀況的,同時為了可以隔離線上和本地環境,一般會把配置文件抽取出來放到線上,避免有人誤操作導致本地的配置內容發布出去。同時線上的配置文件也會在每次變更的時候進行記錄,包括;版本號、時間、MD5、內容信息和操作人。

在後續上線時如果發現緊急問題,系統就會需要回滾操作,如果執行回滾那麼也可以設置配置文件是否回滾。因為每一個版本的系統可能會隨着帶着一些配置文件的信息,這個時候就可以很方便的讓系統與配置文件一起回滾操作。

我們接下來就使用備忘錄模式,模擬如何記錄配置文件信息。實際的使用過程中還會將信息存放到庫中進行保存,這裏暫時只是使用內存記錄。

五、備忘錄模式記錄配置文件版本信息

備忘錄的設計模式實現方式,重點在於不更改原有類的基礎上,增加備忘錄類存放記錄。可能平時雖然不一定非得按照這個設計模式的代碼結構來實現自己的需求,但是對於功能上可能也完成過類似的功能,記錄系統的信息。

除了現在的這個案例外,還可以是運營人員在後台erp創建活動對信息的記錄,方便運營人員可以上下修改自己的版本,而不至於因為誤操作而丟失信息。

1. 工程結構

itstack-demo-design-17-00
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── Admin.java
    │           ├── ConfigFile.java
    │           ├── ConfigMemento.java
    │           └── ConfigOriginator.java
    └── test
        └── java
            └── org.itstack.demo.design.test
                └── ApiTest.java

備忘錄模式模型結構

  • 以上是工程結構的一個類圖,其實相對來說並不複雜,除了原有的配置類(ConfigFile)以外,只新增加了三個類。
  • ConfigMemento:備忘錄類,相當於是對原有配置類的擴展
  • ConfigOriginator:記錄者類,獲取和返回備忘錄類對象信息
  • Admin:管理員類,用於操作記錄備忘信息,比如你一些列的順序執行了什麼或者某個版本下的內容信息

2. 代碼實現

2.1 配置信息類

public class ConfigFile {

    private String versionNo; // 版本號
    private String content;   // 內容
    private Date dateTime;    // 時間
    private String operator;  // 操作人
    
    // ...get/set
}
  • 配置類可以是任何形式的,這裏只是簡單的描述了一個基本的配置內容信息。

2.2 備忘錄類

public class ConfigMemento {

    private ConfigFile configFile;

    public ConfigMemento(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }
    
}
  • 備忘錄是對原有配置類的擴展,可以設置和獲取配置信息。

2.3 記錄者類

public class ConfigOriginator {

    private ConfigFile configFile;

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigMemento saveMemento(){
        return new ConfigMemento(configFile);
    }

    public void getMemento(ConfigMemento memento){
        this.configFile = memento.getConfigFile();
    }

}
  • 記錄者類除了對ConfigFile配置類增加了獲取和設置方法外,還增加了保存saveMemento()、獲取getMemento(ConfigMemento memento)
  • saveMemento:保存備忘錄的時候會創建一個備忘錄信息,並返回回去,交給管理者處理。
  • getMemento:獲取的之後並不是直接返回,而是把備忘錄的信息交給現在的配置文件this.configFile,這部分需要注意。

2.4 管理員類

public class Admin {

    private int cursorIdx = 0;
    private List<ConfigMemento> mementoList = new ArrayList<ConfigMemento>();
    private Map<String, ConfigMemento> mementoMap = new ConcurrentHashMap<String, ConfigMemento>();

    public void append(ConfigMemento memento) {
        mementoList.add(memento);
        mementoMap.put(memento.getConfigFile().getVersionNo(), memento);
        cursorIdx++;
    }

    public ConfigMemento undo() {
        if (--cursorIdx <= 0) return mementoList.get(0);
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento redo() {
        if (++cursorIdx > mementoList.size()) return mementoList.get(mementoList.size() - 1);
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento get(String versionNo){
        return mementoMap.get(versionNo);
    }

}
  • 在這個類中主要實現的核心功能就是記錄配置文件信息,也就是備忘錄的效果,之後提供可以回滾和獲取的方法,拿到備忘錄的具體內容。
  • 同時這裏設置了兩個數據結構來存放備忘錄,實際使用中可以按需設置。List<ConfigMemento>Map<String, ConfigMemento>
  • 最後是提供的備忘錄操作方法;存放(append)、回滾(undo)、返回(redo)、定向獲取(get),這樣四個操作方法。

3. 測試驗證

3.1 編寫測試類

@Test
public void test() {
    Admin admin = new Admin();
    ConfigOriginator configOriginator = new ConfigOriginator();
    configOriginator.setConfigFile(new ConfigFile("1000001", "配置內容A=哈哈", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 保存配置
    configOriginator.setConfigFile(new ConfigFile("1000002", "配置內容A=嘻嘻", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 保存配置
    configOriginator.setConfigFile(new ConfigFile("1000003", "配置內容A=么么", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 保存配置
    configOriginator.setConfigFile(new ConfigFile("1000004", "配置內容A=嘿嘿", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 保存配置  

    // 歷史配置(回滾)
    configOriginator.getMemento(admin.undo());
    logger.info("歷史配置(回滾)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));  

    // 歷史配置(回滾)
    configOriginator.getMemento(admin.undo());
    logger.info("歷史配置(回滾)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));  

    // 歷史配置(前進)
    configOriginator.getMemento(admin.redo());
    logger.info("歷史配置(前進)redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));   

    // 歷史配置(獲取)
    configOriginator.getMemento(admin.get("1000002"));
    logger.info("歷史配置(獲取)get:{}", JSON.toJSONString(configOriginator.getConfigFile()));
}
  • 這個設計模式的學習有一部分重點是體現在了單元測試類上,這裏包括了四次的信息存儲和備忘錄歷史配置操作。
  • 通過上面添加了四次配置后,下面分別進行操作是;回滾1次再回滾1次之後向前進1次最後是獲取指定的版本配置。具體的效果可以參考測試結果。

3.2 測試結果

23:12:09.512 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(回滾)undo:{"content":"配置內容A=嘿嘿","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000004"}
23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(回滾)undo:{"content":"配置內容A=么么","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000003"}
23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(前進)redo:{"content":"配置內容A=嘿嘿","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000004"}
23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(獲取)get:{"content":"配置內容A=嘻嘻","dateTime":159320989432,"operator":"小傅哥","versionNo":"1000002"}

Process finished with exit code 0
  • 從測試效果上可以看到,歷史配置按照我們的指令進行了回滾和前進,以及最終通過指定的版本進行獲取,符合預期結果。

六、總結

  • 此種設計模式的方式可以滿足在不破壞原有屬性類的基礎上,擴充了備忘錄的功能。雖然和我們平時使用的思路是一樣的,但在具體實現上還可以細細品味,這樣的方式在一些源碼中也有所體現。
  • 在以上的實現中我們是將配置模擬存放到內存中,如果關機了會導致配置信息丟失,因為在一些真實的場景里還是需要存放到數據庫中。那麼此種存放到內存中進行回復的場景也不是沒有,比如;Photoshop、運營人員操作ERP配置活動,那麼也就是即時性的一般不需要存放到庫中進行恢復。另外如果是使用內存方式存放備忘錄,需要考慮存儲問題,避免造成內存大量消耗。
  • 設計模式的學習都是為了更好的寫出可擴展、可管理、易維護的代碼,而這個學習的過程需要自己不斷的嘗試實際操作,理論的知識與實際結合還有很長一段距離。切記多多上手!

七、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式「多種類型商品不同接口,統一發獎服務搭建場景」
  • 2. 重學 Java 設計模式:實戰原型模式「上機考試多套試,每人題目和答案亂序排列場景」
  • 3. 重學 Java 設計模式:實戰橋接模式「多支付渠道(微信、支付寶)與多支付模式(刷臉、指紋)場景」
  • 4. 重學 Java 設計模式:實戰組合模式「營銷差異化人群發券,決策樹引擎搭建場景」
  • 5. 重學 Java 設計模式:實戰外觀模式「基於SpringBoot開發門面模式中間件,統一控制接口白名單場景」
  • 6. 重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」
  • 7. 重學 Java 設計模式:實戰備忘錄模式「模擬互聯網系統上線過程中,配置文件回滾場景」

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

※幫你省時又省力,新北清潔一流服務好口碑

從0到1打造數據可信的數據產品:解析數據治理在過程可信變革中的運作流程

摘要:本文針對“數據牽引改進,工具固化規範”這一思路在業務團隊落地過程中的動作流程進行詳細闡述,並明確了支撐整個流程的關鍵角色定義和組織運作形式。

目的

為實現雲服務開發的過程可信,需要基於數據對各個服務產品部的可信變革動作進行數據採集、進展可視、目標牽引、能力評估,最終用數據反映目標達成。與傳統的“基於數據晾曬驅動業務團隊改進,6+1指標度量”的運作方式有本質的區別,我們是基於統一的作業工具上產生的客觀數據呈現,識別研發過程中基本的流程斷裂點和質量缺失動作,和業務團隊達成一致的目標后,把大部分改進動作固話到作業工具中自動化承載,我們稱這個思路為“數據牽引改進,工具固化規範”,也就是我們不僅告訴業務團隊哪裡有問題,同時也要基於我們的作業工具,輔助業務團隊一起改進完善。

本文針對“數據牽引改進,工具固化規範”這一思路在業務團隊落地過程中的動作流程進行詳細闡述,並明確了支撐整個流程的關鍵角色定義和組織運作形式。

數據牽引改進,是指關注軟件交付過程中各種度量數據的收集、統計、分析和反饋,通過可視化的數據客觀反映整個研發過程的狀態,以全局視角分析系統約束點,並和業務團隊達成共識,提煉出客觀有效的改進目標;工具固化規範,針對識別出來的Gap點和重點問題進行分析,制定出可以在作業工具承載的模板規範,以及需要工程師行為做出改變的能力要求,並在作業工具上對這些規範要求的落地效果進行檢查,用數據度量改進效果。最後,對改進項目進行總結分享,打造學習型組織,不斷驅動持續改進和價值交付,牽引研發團隊模式和文化的轉變。

2020年的研發過程可信圍繞CleanCode、構建、開源、E2E追溯四個領域開展,這也是公司要求的可信變革中最基本、最重要、投入產出比最大的四個點。

整體流程說明

整個運作流程,圍繞數據,按照“定義軟件工程規範->定義數據分析模型->工具實現數據度量和分析->數據運營發現實際軟件工程活動和規範的偏差->工具輔助團隊改進->工具固化軟件工程規範”這個流程進行實施,並對最終效果進行階段性總結。隨着業務團隊能力的提升以及軟件工程規範性、開發模式的改變,對最初定義的軟件工程規範,會階段性的進行完善,循環往複、持續優化,最終讓業務團隊在遵守公司要求的研發過程可信規範的前提下,實現業務成功。

1) 定義軟件工程規範:圍繞公司可信變革的目標,BU對各個服務產品部的研發模式規範和能力要求,COE制定適合BU現狀的軟件工程規範;

2) 定義數據模型:COE針對制定的軟件工程規範,提煉出核心的、有針對性、可用工具度量的數據模型,並且和各個服務產品部達成一致;

3) 工具實現數據度量和分析:根據這幾個數據模型,數據分析工具自動從數據源進行採集、匯總、計算,並把結果呈現在數據看板上;業務團隊可以打開匯總數據,根據明細數據進行動作規範自檢和改進;

4) 數據運營發現實際軟件工程活動和規範的偏差:數據治理小組在實際運營過程中,分析度量指標的數據,識別業務團隊實際的軟件工程活動和要求規範不一致的Gap點和關鍵問題;

5) 工具輔助業務團隊改進:COE針對分析出來的Gap點和關鍵問題,制定相應的改進措施,作業工具承載流程規範模板化整改,並針對業務團隊的不規範行為,制定適合各個服務產品部的公約要求,促使業務團隊人員能力提升;

6) 工具固化軟件工程規範:針對業務團隊的公約要求,在作業工具上進行check,最終作業工具承載了整個軟件工程規範要求,以及融入到作業流程中的規範要求事前檢查。

三層數據分析模型

我們採用了三層數據分析模型,由作業工具自動採集用戶研發過程行為明細數據,數據分析工具進行准實時匯總計算呈現總體目標,三層數據系統性的輔助業務團隊系統性的識別研發過程中的不規範點和能力短板,讓業務團隊“知其然,知其所以然”。這三層數據模型是層層深入,迭代完善,下層支撐上層的關係。

第一層:目標、進展、結果數據分析;和公司可信變革目標牽引對齊,結合BU實際情況,形成BU的整體可信要求,並在數據分析看板上呈現各個服務產品部要達成的過程可信目標、每日的改進進展和最終完成情況;例如,對各個服務產品部要求達成CleanCode的目標。

第二層:詞法/語法分析數據;COE針對第一層的目標牽引,分解出來的具體實施環節的度量指標,只有這些分解的指標都完成,第一層的目標才達成。這一層數據的目的主要是圍繞幫助業務團隊分析自己的能力短板在哪裡,進行有針對性的改進指;通過打開匯總數據的層層下鑽,用明細數據來分析業務團隊在DevSecOps軟件工程規範流程中關鍵動作執行的缺失點,並針對性的制定改進規範要求,牽引作業工具或者業務團隊補齊該部分缺失動作;例如,CleanCode的過程可信目標達成,可以分解成:靜態檢查配置合規率、Committer合入保障率、代碼倉Clean三個目標,只有這三個目標達成,就可以認為CleanCode總體目標達成。

第三層:語義分析數據:COE打開第二層數據,不僅要看這些關鍵動過做沒做,還要看做的效果怎麼樣,最終效果體現在業務團隊的DevSecOps軟件工程規範提升;這一層的數據分析聚焦在防止為了指標而做第二層的數據,而是看業務團隊是否在真正參考BU制定的規範牽引的目標,提升業務交付過程中的效能、可信、質量能力,以及最終產生實際的業務效果。通過打開各個團隊的明細數據分析審視業務團隊執行的關鍵動作是否符合規範,是否在合適的階段點執行,執行效果是否有效;並階段性的總結和提煉經驗,形成知識資產固化到作業工具。例如,針對第二層的靜態檢查配置合規率,可以分解為:靜態檢查配置有效性和靜態檢查執行有效性。靜態檢查配置有效性,包括:檢查靜態檢查工具配置的數量、是否符合BU的配置規範,以及是否在代碼合入主幹master時進行了配置;靜態檢查執行有效性,主要看是否每一次MR提交時都執行靜態檢查、是否發現問題在研發活動的最早階段,攔截的問題的效果怎麼樣。只有第三層的動作度量都達成后,才可以說第二層的目標是達成的。

數據治理過程流程圖

為了實現“數據牽引改進,工具固化規範”這個目標,準確、一致、准實時的數據是核心關鍵,但因為數據採集不完整、業務團隊不規範、數據呈現維度不一致等原因,數據的準確性有一個不斷提升的過程,因此需要對各個層級展示的數據進行治理。整個數據治理過程中,由“業務團隊/作業工具/治理小組/數據分析工具(阿基米德)/COE”五個角色緊密配合,而且以年/半年為目標,不斷總結經驗,循環往複、持續優化的過程。

a) COE:和公司可信變革目標牽引對齊,結合BU能力現狀,形成BU的整體可信要求;

b) COE:針對BU的業務現狀,定義出適合BU現狀的軟件工程規範要求;業務團隊:和BU發布的各個領域的軟件工程規範牽引目標達成一致;

c) COE:針對規範分解出核心的度量指標,並制定度量數據模型;

d) 研發用戶:在使用作業工具進行研發活動;作業工具:承載了BU各個服務產品部在使用過程中沉澱的行為數據;

e) 數據分析(阿基米德):准實時接入作業工具的數據,展示各個服務產品部當前的研發能力現狀;

f) COE:和各個服務產品部達成一致,制定各個服務產品部的年度牽引目標;

g) 數據分析(阿基米德):用數據呈現各個服務產品部的牽引目標和能力現狀,統一數據口徑;呈現月/周/天的明細數據,以及支撐Gap分析和重點問題的數據視圖;

h) COE:根據牽引目標和能力現狀,分析Gap原因和關鍵問題;治理小組:在數據運營過程中,根據數據分析團隊當前的能力現狀是否和數據呈現一致;

i) 研發用戶:可以實時登錄數據工具(阿基米德)進行查看各個層級的明細數據;

j) 治理小組:根據准實時進展數據,分析當前團隊研發過程中的實際問題,並匯總給COE;

k) COE:結合細粒度的分析數據、以及治理小組匯總出來的各個服務產品部的實際問題,制定規範和改進措施,包括作業工具的規範和研發用戶的動作行為公約;

l) 作業工具:承載作業工具上落地的規範要求;治理小組:作為接口人,承接研發工程師的行為規範公約,結合各個服務產品部實際情況來負責落地;

m) 研發用戶:按照規範要求和針對數據的自檢進行研發過程行為規範化;

n) 研發工具:對研發用戶的行為規範是否滿足要求進行自動化檢查;最終目標是讓整個軟件工程規範都固化在工具中進行承載;

o) 數據分析(阿基米德):呈現按照規範改進后的明細數據和匯總目標;研發用戶:自助查看整改后的明細數據;

p) COE:根據數據改進的效果,以及過程中暴露的問題進行總結后形成經驗資產,並持續改進;

數據流圖

過程可信的數據在各個工具系統中採集、流轉、匯聚、分析、呈現,整個數據流圖如下:

其中,識別出6個重要的全量數據源:

a) 代碼庫數據:該數據由伏羲的服務信息樹上配置的代碼庫數據,加上阿基米德上人工配置的代碼庫,構成各個雲服務發布到生產倉的代碼全集;

b) Workitem信息流數據:當前識別vision上的需求、問題、task,加上Gitlab/Codeclub上的issue,構成可識別的Workitem數據全集;

c) SRE現網包數據:包括普羅部署、CCE、CPS、CDK各種類型部署的包數據,構成全量現網包數據;

d) 開源二進制包數據:開源中心倉數據(java、python、go、nodejs四種)語言,加上公司c/c++的數據構成全量開源二進制包數據;

e) 研發過程配置數據:阿基米德上配置的committer數據是全量的committer數據;阿基米德上識別出來的主分支是全量的主分支(邏輯“master”)數據;

f) 伏羲研發過程數據:伏羲三個庫,MongoDB的靜態檢查、門禁數據;MySQL中的測試、發布數據;MySQL中包和多個流水線的對應關係數據;一起構成了以“包”為維度的全量伏羲研發過程數據;

運作組織

數據治理運營團隊

按照過程可信在BU的落地策略,在CleanCode、構建、開源、E2E追溯四個領域設置數據治理運營團隊,由 “數據分析工具(阿基米德)—COE—各個服務產品部接口人組成的治理小組”三個角色組成,以“指標度量為牽引,數據的客觀呈現為落地方式,業務的價值反饋為最終目的”的原則來落地數據治理工作。

COE的職責:

1) 和公司可信變革目標牽引對齊,結合BU能力現狀,形成BU的整體可信要求;定義出適合BU現狀的軟件工程規範要求;針對規範分解出核心的度量指標,並制定度量數據模型;

2) 利用作業工具已經產生的數據,和治理小組一起分析識別數據質量的問題,按照三層數據分析模型,層層打開,識別業務團隊能力Gap點。

3) 分析典型問題,識別作業流的斷裂點進行補齊,和業務團隊的不規範動作,制定規範和公約要求,逐步改善數據質量。

4) 事後歸納總結,識別出流程缺失,組織缺失,責任缺失等機制行問題,並固化到作業工具中。

治理小組:

1) 結合各個服務產品部的實際情況,承接COE的數據治理規範在各個服務產品部的落地;

2) 識別數據治理動作在各個服務產品部落地過程中的實際問題,和COE一起分析,提出系統性的解決思路,最終固化到作業工具中。

3) 跟蹤過程可信在業務團隊落地的過程中的進展,為業務團隊最終達成可信變革目標負責,為改進過程產生實際的業務價值負責;

數據分析工具(阿基米德):

1) 確保接入的數據準確、實時、一致,用數據實時反映BU各個服務產品部的能力現狀,為COE和治理小組的數據運營提供數據支撐;

2) 系統性的落地COE的方案設計,實現整個BU統一標準的數據看板,能夠清晰的通過數據識別出來業務團隊的能力Gap,牽引業務團隊達成整體改進目標;

3) 按照三層數據模型進行數據展示,層層下鑽,讓業務團隊“知其然,知其所以然”,牽引業務團隊中的每一個人都能自己進行改進;

4) 通過數據分析,識別DevSecOps軟件工程規範在BU的業務團隊落地過程中的重點問題,以及該問題背後的流程、制度缺失,促使最終規範固化在作業工具中。

例會設置

“數據驅動DevSecOps能力提升例會”為研發領域數據治理相關問題的求助和裁決例會。

會議分為三個階段:

1) 第一階段,例行議題,形式類似於“體檢報告”,用數據反映業務團隊的現狀和問題;

2) 第二階段,申報議題,形式類似於“專家會診”,討論某一個具體數據治理過程中的問題和Top困難求助;

3) 第三階段,靈活安排議題,形式類似於“問題總結”,針對某一類的具體問題,進行集中討論和歸納總結定義,形成BU的規範流程和章程總結。

主數據承載系統

主數據是指具有高業務價值的、可以在企業內跨越多個業務部門被重複使用的數據,是單一、準確、權威的數據來源。和業務型數據、分析型數據相比,主數據主要有以下幾個特徵:

1) 特徵一致性:也就是能否保證主數據的關鍵特徵在不同應用、不同系統中的高度一致,直接關係了數據治理成敗;

2) 識別唯一性:在一個系統、一個平台,甚至一個企業範圍內,同一主數據實體要求具有唯一的數據標識,即數據編碼;

3) 長期有效性:貫穿該業務對象的整個生命周期甚至更長,當該主數據失去其效果時,系統採取的措施通常為軟刪除,而不是物理刪除;

4) 業務穩定性:在業務過程中其識別信息和關鍵特徵會被業務過程中產生的數據繼承、引用和複製。除非該主數據本身的特徵發生變化,否則該主數據不會隨着業務的過程中被其他系統修改。

主數據源識別原則:

a) 如果有多個數據源構成同類型數據的主數據,兩種處理策略:

1)選取一個源系統逐步收編其他源系統的數據,變成唯一主數據源

2)如果1)不能實現,由阿基米德系統進行封裝后屏蔽多個數據源系統,該類型數據的唯一數據源變成阿基米德,待後續1)實現后,阿基米德該類型主數據源失效。

3)當數據在多個作業系統中進行流轉時,判斷是否作為主數據源的標準是:數據在該系統有實際的業務動作產生,而不是只承載數據的流轉。

b) 如果確定為唯一數據源,其他消費該類型數據的系統不能和數據源產生衝突。

所有數據僅能在數據源產生,其它系統只能讀取不能修改。下游發現的數據源質量問題,應當在數據源頭進行修正。

c) 主數據使用方不得改變原始數據,但可以進行擴展。

數據消費方不得對獲取的數據進行增、刪、改,但可以在數據的基礎上進行屬性擴展。

d) 在滿足信息安全的前提下充分共享,不得拒絕合理的數據共享需求。

數據如果不流轉,不僅不會產生業務價值,還增加存儲成本;只有不斷流轉,對業務團隊產生實際價值時,還能得到使用效果的反饋,促進數據價值的進一步提升。

原則為:核心資產安全優先,非關鍵資產效率優先。

一類主數據源

二類主數據源

 

點擊關注,第一時間了解華為雲新鮮技術~

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

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

聯合國:反疫苗運動 助長薩摩亞麻疹疫情惡化

摘錄自2019年12月6日中央通訊社綜合報導

聯合國兒童基金會(UNICEF)太平洋島嶼負責人今天(5日)表示,社群媒體巨頭必須嚴厲取締反疫苗接種貼文,這些貼文助長薩摩亞(Samoa)致命麻疹疫情惡化。

UNICEF地區代表耶特(Sheldon Yett)表示,推特(Twitter)、臉書(Facebook)和Instagram(IG)等網路平台上「極不負責任」的反疫苗接種訊息,加劇了薩摩亞爆發的麻疹疫情,自10月中旬以來已造成62人死亡。耶特告訴法新社:「很明顯地它們必需負起企業責任並展開行動,確保那些人民,特別是弱勢族群能獲得正確資訊,讓孩童得以存活。」

在麻疹疫情爆發前,薩摩亞的疫苗接種率降至只剩略超過30%,遠低於公認最佳接種率90%,這也使得該海島國家極易受到感染。世界衛生組織(WHO)把矛頭指向反疫苗宣傳運動。耶特表示,這項運動主要是由海外倡議人士在網路上展開。

「很不幸的是,這項運動在薩摩亞找到願意相信的民眾,那裡有一部分人懷疑醫療保健服務品質,且可能不信任當地(疫苗)供應者。」他說,來自諸如美國和澳洲等富裕已開發國家的運動人士,在網路上張貼反疫苗訊息,他們必須意識到自己的所作所為會對開發中國家帶來衝擊。

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

※幫你省時又省力,新北清潔一流服務好口碑

陷垃圾危機 菲律賓計劃禁用一次性塑膠

摘錄自2019年12月5日中央通訊社綜合報導

菲律賓環境部長希瑪圖今天(5日)說,由於人們製造數量甚多的廢棄物,清理速度遠遠趕不及,菲律賓正處於垃圾危機中。環境部預計將在2週內規劃完成限用一次性塑膠的全國禁令。

ABS-CBN新聞網和「菲律賓每日詢問報」(Philippine Daily Inquirer)報導,希瑪圖(Roy Cimatu)說,在馬尼拉都會區,今年第一季製造的廢棄物達3萬4574.77立方公尺,第二季則為3萬2221.17立方公尺,已超過全年基線預估值5萬8112.31立方公尺。

他引述數據表示,菲律賓是全球第3大海洋塑膠污染來源國。為此,當局須加強固體廢棄物管理政策。菲律賓總統杜特蒂(Rodrigo Duterte)日前提出為因應氣候變遷問題,菲律賓應禁用塑膠。

希瑪圖說,除了一次性塑膠禁令,環境暨天然資源部(DENR)正擬定的命令也將涵括塑膠回收問題。

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

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

自動任務調度 – Timer

一、概述:

最近維護一個老項目,裏面使用的是Timer的時間調度器,以前沒接觸過,對着代碼鼓搗了半天,查閱了部分博客,最後總結出自己的見解,新項目一般是不會用這種老掉牙的時間調度器了,但是維護老項目還是用的着的。就當筆記記錄一下了,自己寫的才是符合自己的思路走向的。有時間再補上Quartz調度器,這個才是現在使用最多的。

二、常用的三種調度器分類

Java自帶的java.util.Timer類,這個類允許你調度一個java.util.TimerTask任務。使用這種方式可以讓你的程序按照某一個頻度執行,但不能在指定時間運行。

使用Quartz,這是一個功能比較強大的的調度器,可以讓你的程序在指定時間執行,也可以按照某一個頻度執行,配置起來稍顯複雜。

Spring3.0以後自帶的task,可以將它看成一個輕量級的Quartz,而且使用起來比Quartz簡單許多。

三、使用Spring體系來完成代碼的搭建

1、代碼結構:

                                  

 

 

 

2、springContext.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
       default-lazy-init="true">

    <!--定義了一個TimerFactoryBean類,並且把ScheduledTimerTask類的實例作為需要調度的task。-->
    <bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean" lazy-init="false">
        <property name="scheduledTimerTasks">
            <list>
                <ref local="scheduledTask1"/>
                <ref local="scheduledTask2"/>
            </list>
        </property>
    </bean>

    <!--利用ScheduledTimerTask類來配置每個task的啟動時間延時,每次啟動之間的間隔,當然還有最重要的是需要運行那個對象,也就是MethodInvokingTimerTaskFactoryBean類的實例-->
    <bean id="scheduledTask1" class="org.springframework.scheduling.timer.ScheduledTimerTask">
        <property name="delay" value="0" />
        <property name="period" value="1000" />
        <property name="timerTask">
            <ref bean="methodInvokingTask1"/>
        </property>
    </bean>

    <bean id="scheduledTask2" class="org.springframework.scheduling.timer.ScheduledTimerTask">
        <property name="delay" value="0" />
        <property name="period" value="1000" />
        <property name="timerTask">
            <ref bean="methodInvokingTask2"/>
        </property>
    </bean>

    <!--利用spring提供的MethodInvokingTimerTaskFactoryBean類來實現來實現對對task類和方法的聲明,聲明目標對象和方法,從而使spring知道要運行那個類的那個方法-->
    <bean id="methodInvokingTask1" class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean">
        <property name="targetObject" ref="myTask1"/>
        <property name="targetMethod" value="run"/>
    </bean>

    <bean id="methodInvokingTask2" class="org.springframework.scheduling.timer.MethodInvokingTimerTaskFactoryBean">
        <property name="targetObject" ref="myTask2"/>
        <property name="targetMethod" value="run"/>
    </bean>

    <!--被指定自動任務的類對象-->
    <bean id="myTask1" class="com.turtle.test.MyTask">
        <property name="name" value="啟動一"/>
    </bean>

    <bean id="myTask2" class="com.turtle.test.MyTask_2">
        <property name="name" value="啟動二"/>
    </bean>

</beans>

 

3、MyTask文件

package com.turtle.test;

import java.util.TimerTask;

/**
 * 自定義一個定時任務
 * 推薦是繼承自 TimerTask
 */
public class MyTask extends TimerTask {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private static int i = 0;

    // 使用線程中的方法  run
    @Override
    public void run() {
        System.out.println("定時任務啟動"+name+"----出現了"+i++);
    }
}

 

4、MyTask_2文件

package com.turtle.test;

import java.util.TimerTask;

/**
 * 自定義一個定時任務
 * 推薦是繼承自 TimerTask
 */
public class MyTask_2 extends TimerTask {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    private static int i = 0;

    // 使用線程中的方法  run
    @Override
    public void run() {
        System.out.println("定時任務啟動"+name+"----出現了"+i++);
    }
}

 

 

5、MyTestTask_Test_01

package com.turtle.test;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class MyTestTask_Test_01 {
    public static void main(String[] args) {
        // 啟動測試
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("springContext.xml");
    }
}

 

 

6、結果:

 

                        

四、總結:

如果要使用TImer的調度器的話,推薦使用新的ScheduledExecutorService,這個目前沒使用,就沒進行代碼驗證了,推薦一博客,大概看了下,寫得挺好的

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

※幫你省時又省力,新北清潔一流服務好口碑

用python實現對元素的長截圖

一.目標

瀏覽網頁的時候,看見哪個元素,就能截取哪個元素當圖片,不管那個元素有多長

 

二.所用工具和第三方庫

python ,PIL,selenium

pycharm

三.代碼部分

長截圖整體思路:

1.獲取元素

2.移動,截圖,移動,截圖,直到抵達元素的底部

3.把截圖按照元素所在位置切割,在所有圖片中只保留該元素

4.拼接

 

如果driver在環境變量中,那麼不用指定路徑

b=webdriver.Chrome(executable_path=r"C:\Users\Desktop\chromedriver.exe")#指定一下driver
b.get("")
b.maximize_window()#最大化窗口

打開網站

 

 

 我們可以看見一個ID為maincontent的元素,寬度為850PX,長度為3828PX,這個長度必須使用才能長截圖才能完整截下來

 

el=b.find_element_by_id("maincontent")#找到元素

我們還需要一個重要的參數,就是你電腦一次能截取多高的像素

先用下圖代碼獲取一個圖片

#fp為存放圖片的地址
b.get_screenshot_as_file(fp)

 

也就是說用我電腦上截圖的默認高度為614像素

 

 所以我設置一個變量:

sc_hight=614

然後設置一下其他變量

    count = int(el.size["height"] / sc_hight)  # 元素的高度除以你每次截多少就是次數
    start_higth = el.location["y"]  # 元素的初始高度
    max_px = start_higth + (count - 1) * sc_hight  # for循環中最大的px
    last_px = el.size["height"] + start_higth - sc_hight  # 元素最底部的位置
    surplus_px = last_px - max_px  # 剩餘的邊的高度
    img_path = []  # 用來存放圖片地址

註釋:

1.count為元素的高度/每次截取的高度,比如這次實例中元素高度為3828PX,我每次截614px,需要6.2次,int之後變成6,也就是截6次,還剩一點,那一點後面再說

2.start_higth為初始高度,這個沒有什麼可說的

3.max_px為循環結束后,到達的高度

4.last_px為元素最底部的高度

5.surplus_px就是移動6次后,還沒有截取的高度

屏幕每次移動,移動sc_hight個像素,初始位置為(0,元素的Y值)

    for i in range(0, count):
        js = "scrollTo(0,%s)" % (start_higth + i * sc_hight)  # 用於移動滑輪,每次移動614px,初始值是元素的初始高度
        b.execute_script(js)  # 執行js
        time.sleep(0.5)
        fp = r"C:\Users\wdj\Desktop\%s.png" % i  # 圖片地址,運行的話,改一下
        b.get_screenshot_as_file(fp)  # 屏幕截圖,這裡是截取是完整的網頁圖片,你可以打斷點看一下圖片
        img = Image.open(fp=fp)
        img2 = img.crop((el.location["x"], 0, el.size["width"] + el.location["x"], sc_hight))  # 剪切圖片
        img2.save(fp)  # 保存圖片,覆蓋完整的網頁圖片
        img_path.append(fp)  # 添加圖片路徑
        time.sleep(0.5)
        print(js)
    else:
        js = "scrollTo(0,%s)" % last_px  # 滾動到最後一個位置
        b.execute_script(js)
        fp = r"C:\Users\wdj\Desktop\last.png"
        b.get_screenshot_as_file(fp)
        img = Image.open(fp=fp)
        print((el.location["x"], sc_hight - surplus_px, el.size["width"] + el.location["x"], sc_hight))
        img2 = img.crop((el.location["x"], sc_hight - surplus_px, el.size["width"] + el.location["x"], sc_hight))
        img2.save(fp)
        img_path.append(fp)
        print(js)

上面是把該元素的在頁面都截完,並且剪切,把圖片保存的路徑放入img_path

最後一步:把所有截圖都貼到新創建的圖片中

    new_img = Image.new("RGB", (el.size["width"], el.size["height"]))  # 創建一個新圖片,大小為元素的大小
    k = 0
    for i in img_path:
        tem_img = Image.open(i)
        new_img.paste(tem_img, (0, sc_hight * k))  # 把圖片貼上去,間隔一個截圖的距離
        k += 1
    else:
        new_img.save(r"C:\Users\wdj\Desktop\test.png")  # 保存

 

運行效果圖:


說明完整的截取下來了

 

 

 

補充優化:

如果是個小元素怎麼辦,不用長截圖就能截取的那種

因為很簡單我就直接貼代碼了

    start_higth = el.location["y"]
    js = "scrollTo(0,%s)" % (start_higth)
    b.execute_script(js)  # 執行js
    time.sleep(0.5)
    fp = r"C:\Users\wdj\Desktop\test.png" # 圖片地址,運行的話,改一下
    b.get_screenshot_as_file(fp)
    img = Image.open(fp=fp)
    img2 = img.crop((el.location["x"], 0, el.size["width"] + el.location["x"], el.size["height"]))  # 剪切圖片
    img2.save(fp)

效果如下:

 

 

完整代碼:

from selenium import webdriver
from PIL import Image
import time
def short_sc(el,b):
    start_higth = el.location["y"]
    js = "scrollTo(0,%s)" % (start_higth)
    b.execute_script(js)  # 執行js
    time.sleep(0.5)
    fp = r"C:\Users\wdj\Desktop\test.png" # 圖片地址,運行的話,改一下
    b.get_screenshot_as_file(fp)
    img = Image.open(fp=fp)
    img2 = img.crop((el.location["x"], 0, el.size["width"] + el.location["x"], el.size["height"]))  # 剪切圖片
    img2.save(fp)

def long_sc(el,b):
    count = int(el.size["height"] / sc_hight)  # 元素的高度除以你每次截多少就是次數
    start_higth = el.location["y"]  # 元素的初始高度
    max_px = start_higth + (count - 1) * sc_hight  # for循環中最大的px
    last_px = el.size["height"] + start_higth - sc_hight  # 元素最底部的位置
    surplus_px = last_px - max_px  # 剩餘的邊的高度
    img_path = []  # 用來存放圖片地址
    for i in range(0, count):
        js = "scrollTo(0,%s)" % (start_higth + i * sc_hight)  # 用於移動滑輪,每次移動614px,初始值是元素的初始高度
        b.execute_script(js)  # 執行js
        time.sleep(0.5)
        fp = r"C:\Users\wdj\Desktop\%s.png" % i  # 圖片地址,運行的話,改一下
        b.get_screenshot_as_file(fp)  # 屏幕截圖,這裡是截取是完整的網頁圖片,你可以打斷點看一下圖片
        img = Image.open(fp=fp)
        img2 = img.crop((el.location["x"], 0, el.size["width"] + el.location["x"], sc_hight))  # 剪切圖片
        img2.save(fp)  # 保存圖片,覆蓋完整的網頁圖片
        img_path.append(fp)  # 添加圖片路徑
        time.sleep(0.5)
        print(js)
    else:
        js = "scrollTo(0,%s)" % last_px  # 滾動到最後一個位置
        b.execute_script(js)
        fp = r"C:\Users\wdj\Desktop\last.png"
        b.get_screenshot_as_file(fp)
        img = Image.open(fp=fp)
        print((el.location["x"], sc_hight - surplus_px, el.size["width"] + el.location["x"], sc_hight))
        img2 = img.crop((el.location["x"], sc_hight - surplus_px, el.size["width"] + el.location["x"], sc_hight))
        img2.save(fp)
        img_path.append(fp)
        print(js)

    new_img = Image.new("RGB", (el.size["width"], el.size["height"]))  # 創建一個新圖片,大小為元素的大小
    k = 0
    for i in img_path:
        tem_img = Image.open(i)
        new_img.paste(tem_img, (0, sc_hight * k))  # 把圖片貼上去,間隔一個截圖的距離
        k += 1
    else:
        new_img.save(r"C:\Users\wdj\Desktop\test.png")  # 保存

b=webdriver.Chrome(executable_path=r"C:\Users\wdj\Desktop\chromedriver.exe")#指定一下driver
b.get("https://www.w3school.com.cn/html/html_links.asp")
b.maximize_window()#最大化窗口
# b.get_screenshot_as_file(fp)
sc_hight=614#你屏幕截圖默認的大小,可以去截一張,去畫圖裡面看看是多少像素,我這裡是614像素

# b.switch_to.frame(b.find_element_by_xpath('//*[@id="intro"]/iframe'))
el=b.find_element_by_id("maincontent")#找到元素
if el.size["height"]>sc_hight:
    long_sc(el,b)
else:
    short_sc(el,b)

完整代碼

 

PS:

有些特殊情況,比如截取的元素在iframe中,直接用driver.switch_to.frame(iframe元素)即可

或者不是iframe,但是元素有overflow屬性,直接用JS把他的overflow去掉就行

 

 

 

 

 

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

【其他文章推薦】

※想知道網站建置網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計後台網頁設計

※不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

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

※帶您來看台北網站建置台北網頁設計,各種案例分享

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

通俗地說邏輯回歸【Logistic regression】算法(二)sklearn邏輯回歸實戰

前情提要:

上一篇主要介紹了邏輯回歸中,相對理論化的知識,這次主要是對上篇做一點點補充,以及介紹sklearn 邏輯回歸模型的參數,以及具體的實戰代碼。

1.邏輯回歸的二分類和多分類

上次介紹的邏輯回歸的內容,基本都是基於二分類的。那麼有沒有辦法讓邏輯回歸實現多分類呢?那肯定是有的,還不止一種。

實際上二元邏輯回歸的模型和損失函數很容易推廣到多元邏輯回歸。比如總是認為某種類型為正值,其餘為0值。

舉個例子,要分類為A,B,C三類,那麼就可以把A當作正向數據,B和C當作負向數據來處理,這樣就可以用二分類的方法解決多分類的問題,這種方法就是最常用的one-vs-rest,簡稱OvR。而且這種方法也可以方便得推廣到其他二分類模型中(當然其他算法可能有更好的多分類辦法)。

另一種多元邏輯回歸的方法是Many-vs-Many(MvM),它會選擇一部分類別的樣本和另一部分類別的樣本來做邏輯回歸二分類。

聽起來很不可思議,但其實確實是能辦到的。比如數據有A,B,C三個分類。

我們將A,B作為正向數據,C作為負向數據,訓練出一個分模型。再將A,C作為正向數據,B作為負向數據,訓練出一個分類模型。最後B,C作為正向數據,C作為負向數據,訓練出一個模型。

通過這三個模型就能實現多分類,當然這裏只是舉個例子,實際使用中有其他更好的MVM方法。限於篇幅這裏不展開了。

MVM中最常用的是One-Vs-One(OvO)。OvO是MvM的特例。即每次選擇兩類樣本來做二元邏輯回歸。

對比下兩種多分類方法,通常情況下,Ovr比較簡單,速度也比較快,但模型精度上沒MvM那麼高。MvM則正好相反,精度高,但速度上比不過Ovr。

2.邏輯回歸的正則化

所謂正則化,其目的是為了減弱邏輯回歸模型的精度,難道模型的準確度不是越高越好嘛?看看下面這張圖就明白了:

左邊那個圖就是過擬合的情況,過擬合其實就是模型的精度太過高了,它能非常好得匹配訓練集的數據,但一旦有新的數據,就會表現得很差。

而我們要的非過擬合的模型是,精度可以差一些,但泛化性能,也就是對新的數據的識別能力,要比較好。

正則化就是減弱模型精度,提高泛化效果的這個東西。

3.sklearn各個參數

def LogisticRegression(penalty='l2', 
                                    dual=False, 
                                    tol=1e-4, 
                                    C=1.0,
                                    fit_intercept=True, 
                                    intercept_scaling=1, 
                                    class_weight=None,
                                    random_state=None, 
                                    solver='warn', 
                                    max_iter=100,
                                    multi_class='warn', 
                                    verbose=0, 
                                    warm_start=False, 
                                    n_jobs=None,
                                    l1_ratio=None
                                    )
跟線性回歸一比,邏輯回歸的參數那還真是多啊,不過我們一個一個來看看參數都是什麼意思吧。                                 

- dual:對偶或者原始方法,布爾類型,默認為False。Dual只適用於正則化相為l2的‘liblinear’的情況,通常樣本數大於特徵數的情況下,默認為False。

- tol:停止迭代求解的閾值,單精度類型,默認為1e-4。

- C:正則化係數的倒數,必須為正的浮點數,默認為 1.0,這個值越小,說明正則化效果越強。換句話說,這個值越小,越訓練的模型更泛化,但也更容易欠擬合。

- fit_intercept:是否要使用截距(在決策函數中使用截距),布爾類型,默認為True。

- intercept_scaling:官方解釋比較模糊,我說下個人理解。浮點型,默認值是1.0。這個參數僅在“solver”參數(下面介紹)為“liblinear”“fit_intercept ”參數為True的時候生效。作用是給特徵向量添加一個常量,這個常量就是intercept_scaling。比如原本的向量是[x],那麼添加后就變成[x,intercept_scaling]。

- class_weight:分類權重,可以是一個dict(字典類型),也可以是一個字符串"balanced"字符串。默認是None,也就是不做任何處理,而"balanced"則會去自動計算權重,分類越多的類,權重越低,反之權重越高。也可以自己輸出一個字典,比如一個 0/1 的二元分類,可以傳入{0:0.1,1:0.9},這樣 0 這個分類的權重是0.1,1這個分類的權重是0.9。這樣的目的是因為有些分類問題,樣本極端不平衡,比如網絡攻擊,大部分正常流量,小部分攻擊流量,但攻擊流量非常重要,需要有效識別,這時候就可以設置權重這個參數。

- random_state:設置隨機數種子,可以是int類型和None,默認是None。當"solver"參數為"sag"和"liblinear"的時候生效。

- verbose:輸出詳細過程,int類型,默認為0(不輸出)。當大於等於1時,輸出訓練的詳細過程。僅當"solvers"參數設置為"liblinear"和"lbfgs"時有效。

- warm_start:設置熱啟動,布爾類型,默認為False。若設置為True,則以上一次fit的結果作為此次的初始化,如果"solver"參數為"liblinear"時無效。

- max_iter:最大迭代次數,int類型,默認-1(即無限制)。注意前面也有一個tol迭代限制,但這個max_iter的優先級是比它高的,也就如果限制了這個參數,那是不會去管tol這個參數的。

OK,上述就是對一些比較簡略的參數的說明,但是還有幾個重要的參數沒講到,這是因為這幾個參數我覺得需要單獨拎出來講一講。

sklearn邏輯回歸參數 –penalty

正則化類型選擇,字符串類型,可選’l1’,’l2’,’elasticnet’和None,默認是’l2’,通常情況下,也是選擇’l2’。這個參數的選擇是會影響到參數’solver’的選擇的,下面會介紹。

其中’l1’和’l2’。分別對應L1的正則化和L2的正則化,’elasticnet’則是彈性網絡(這玩意我也不大懂),默認是L2的正則化。

在調參時如果主要的目的只是為了解決過擬合,一般penalty選擇L2正則化就夠了。但是如果選擇L2正則化發現還是過擬合,即預測效果差的時候,就可以考慮L1正則化。另外,如果模型的特徵非常多,我們希望一些不重要的特徵係數歸零,從而讓模型係數稀疏化的話,也可以使用L1正則化。

penalty參數的選擇會影響我們損失函數優化算法的選擇。即參數solver的選擇,如果是L2正則化,那麼4種可選的算法{‘newton-cg’,‘lbfgs’,‘liblinear’,‘sag’}都可以選擇。但是如果penalty是L1正則化的話,就只能選擇‘liblinear’了。這是因為L1正則化的損失函數不是連續可導的,而{‘newton-cg’,‘lbfgs’,‘sag’}這三種優化算法時都需要損失函數的一階或者二階連續導數。而‘liblinear’並沒有這個依賴。最後還有一個’elasticnet’,這個只有solver參數為’saga’才能選。

sklearn邏輯回歸參數 –solver

優化算法參數,字符串類型,一個有五種可選,分別是”newton-cg”,”lbfgs”,”liblinear”,”sag”,”saga。默認是”liblinear”。分別介紹下各個優化算法:

  • a) liblinear:使用了開源的liblinear庫實現,內部使用了坐標軸下降法來迭代優化損失函數。
  • b) lbfgs:擬牛頓法的一種,利用損失函數二階導數矩陣即海森矩陣來迭代優化損失函數。
  • c) newton-cg:也是牛頓法家族的一種,利用損失函數二階導數矩陣即海森矩陣來迭代優化損失函數。
  • d) sag:即隨機平均梯度下降,是梯度下降法的變種,和普通梯度下降法的區別是每次迭代僅僅用一部分的樣本來計算梯度,適合於樣本數據多的時候。
    在優化參數的選擇上,官方是這樣建議的:
  • e)saga:優化的,無偏估計的sag方法。(‘sag’ uses a Stochastic Average Gradient descent, and ‘saga’ uses its improved, unbiased version named SAGA.)
    對小的數據集,可以選擇”liblinear”,如果是大的數據集,比如說大於10W的數據,那麼選擇”sag”和”saga”會讓訓練速度更快。

對於多分類問題,只有newton-cg,sag,saga和lbfgs能夠處理多項損失(也就是MvM的情況,還記得上面說到的多分類嘛?),而liblinear僅處理(OvR)的情況。啥意思,就是用liblinear的時候,如果是多分類問題,得先把一種類別作為一個類別,剩餘的所有類別作為另外一個類別。一次類推,遍歷所有類別,進行分類。

這個的選擇和正則化的參數也有關係,前面說到”penalty”參數可以選擇”l1″,”l2″和None。這裏’liblinear’是可以選擇’l1’正則和’l2’正則,但不能選擇None,’newton-cg’,’lbfgs’,’sag’和’saga’這幾種能選擇’l2’或no penalty,而’saga’則能選怎’elasticnet’正則。好吧,這部分還是挺繞的。

歸納一下吧,二分類情況下,數據量小,一般默認的’liblinear’的行,數據量大,則使用’sag’。多分類的情況下,在數據量小的情況下,追求高精度,可以用’newton-cg’或’lbfgs’以’MvM’的方式求解。數據量一大還是使用’sag’。

當然實際情況下還是要調參多次才能確定參數,這裏也只能給些模糊的建議。

sklearn邏輯回歸參數 –multi_class

multi_class參數決定了我們分類方式的選擇,有 ovr和multinomial兩個值可以選擇,默認是 ovr。
ovr即前面提到的one-vs-rest(OvR),而multinomial即前面提到的many-vs-many(MvM)。如果是二元邏輯回歸,ovr和multinomial並沒有任何區別,區別主要在多元邏輯回歸上。

4.sklearn實例

實例這部分,就直接引用sklearn官網的,使用邏輯回歸對不同種類的鳶尾花進行分類的例子吧。

import numpy as np
import matplotlib.pyplot as plt
from sklearn import linear_model, datasets

# 加載鳶尾花數據
iris = datasets.load_iris()
# 只採用樣本數據的前兩個feature,生成X和Y
X = iris.data[:, :2]  
Y = iris.target

h = .02  # 網格中的步長

# 新建模型,設置C參數為1e5,並進行訓練
logreg = linear_model.LogisticRegression(C=1e5)
logreg.fit(X, Y)

# 繪製決策邊界。為此我們將為網格 [x_min, x_max]x[y_min, y_max] 中的每個點分配一個顏色。
x_min, x_max = X[:, 0].min() - .5, X[:, 0].max() + .5
y_min, y_max = X[:, 1].min() - .5, X[:, 1].max() + .5
xx, yy = np.meshgrid(np.arange(x_min, x_max, h), np.arange(y_min, y_max, h))
Z = logreg.predict(np.c_[xx.ravel(), yy.ravel()])

# 將結果放入彩色圖中
Z = Z.reshape(xx.shape)
plt.figure(1, figsize=(4, 3))
plt.pcolormesh(xx, yy, Z, cmap=plt.cm.Paired)

# 將訓練點也同樣放入彩色圖中
plt.scatter(X[:, 0], X[:, 1], c=Y, edgecolors='k', cmap=plt.cm.Paired)
plt.xlabel('Sepal length')
plt.ylabel('Sepal width')

plt.xlim(xx.min(), xx.max())
plt.ylim(yy.min(), yy.max())
plt.xticks(())
plt.yticks(())

plt.show()

運行上面那段代碼會有如下的結果:

可以看到,已將三種類型的鳶尾花都分類出來了。

小結

邏輯回歸算是比較簡單的一種分類算法,而由於簡單,所以也比較適合初學者初步接觸機器學習算法。學習了之後,對後面一些更複雜的機器學習算法,諸如Svm,或更高級的神經網絡也能有一個稍微感性的認知。

而實際上,Svm可以看作是邏輯回歸的更高級的演化。而從神經網絡的角度,邏輯回歸甚至可以看作一個最初級,最淺層的神經網絡。

邏輯回歸就像是金庸小說裏面,獨孤九劍的第一式,最為簡單,卻又是其他威力極大的招式的基礎,其他的招式都又第一式演化而出。

夯實基礎,才能砥礪前行。

以上~

參考文章:

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

※幫你省時又省力,新北清潔一流服務好口碑

Java I/O體系從原理到應用,這一篇全說清楚了

本文介紹操作系統I/O工作原理,Java I/O設計,基本使用,開源項目中實現高性能I/O常見方法和實現,徹底搞懂高性能I/O之道

基礎概念

在介紹I/O原理之前,先重溫幾個基礎概念:

  • (1) 操作系統與內核

操作系統:管理計算機硬件與軟件資源的系統軟件
內核:操作系統的核心軟件,負責管理系統的進程、內存、設備驅動程序、文件和網絡系統等等,為應用程序提供對計算機硬件的安全訪問服務

  • 2 內核空間和用戶空間

為了避免用戶進程直接操作內核,保證內核安全,操作系統將內存尋址空間劃分為兩部分:
內核空間(Kernel-space),供內核程序使用
用戶空間(User-space),供用戶進程使用
為了安全,內核空間和用戶空間是隔離的,即使用戶的程序崩潰了,內核也不受影響

  • 3 數據流

計算機中的數據是基於隨着時間變換高低電壓信號傳輸的,這些數據信號連續不斷,有着固定的傳輸方向,類似水管中水的流動,因此抽象數據流(I/O流)的概念:指一組有順序的、有起點和終點的字節集合

抽象出數據流的作用:實現程序邏輯與底層硬件解耦,通過引入數據流作為程序與硬件設備之間的抽象層,面向通用的數據流輸入輸出接口編程,而不是具體硬件特性,程序和底層硬件可以獨立靈活替換和擴展

I/O 工作原理

1 磁盤I/O

典型I/O讀寫磁盤工作原理如下:

tips: DMA:全稱叫直接內存存取(Direct Memory Access),是一種允許外圍設備(硬件子系統)直接訪問系統主內存的機制。基於 DMA 訪問方式,系統主內存與硬件設備的數據傳輸可以省去CPU 的全程調度

值得注意的是:

  • 讀寫操作基於系統調用實現
  • 讀寫操作經過用戶緩衝區,內核緩衝區,應用進程並不能直接操作磁盤
  • 應用進程讀操作時需阻塞直到讀取到數據

2 網絡I/O

這裏先以最經典的阻塞式I/O模型介紹:

tips:recvfrom,經socket接收數據的函數

值得注意的是:

  • 網絡I/O讀寫操作經過用戶緩衝區,Sokcet緩衝區
  • 服務端線程在從調用recvfrom開始到它返回有數據報準備好這段時間是阻塞的,recvfrom返回成功后,線程開始處理數據報

Java I/O設計

1 I/O分類

Java中對數據流進行具體化和實現,關於Java數據流一般關注以下幾個點:

  • (1) 流的方向
    從外部到程序,稱為輸入流;從程序到外部,稱為輸出流

  • (2) 流的數據單位
    程序以字節作為最小讀寫數據單元,稱為字節流,以字符作為最小讀寫數據單元,稱為字符流

  • (3) 流的功能角色

從/向一個特定的IO設備(如磁盤,網絡)或者存儲對象(如內存數組)讀/寫數據的流,稱為節點流
對一個已有流進行連接和封裝,通過封裝后的流來實現數據的讀/寫功能,稱為處理流(或稱為過濾流);

2 I/O操作接口

java.io包下有一堆I/O操作類,初學時看了容易搞不懂,其實仔細觀察其中還是有規律:
這些I/O操作類都是在繼承4個基本抽象流的基礎上,要麼是節點流,要麼是處理流

2.1 四個基本抽象流

java.io包中包含了流式I/O所需要的所有類,java.io包中有四個基本抽象流,分別處理字節流和字符流:

  • InputStream
  • OutputStream
  • Reader
  • Writer

2.2 節點流

節點流I/O類名由節點流類型 + 抽象流類型組成,常見節點類型有:

  • File文件
  • Piped 進程內線程通信管道
  • ByteArray / CharArray (字節數組 / 字符數組)
  • StringBuffer / String (字符串緩衝區 / 字符串)

節點流的創建通常是在構造函數傳入數據源,例如:

FileReader reader = new FileReader(new File("file.txt"));
FileWriter writer = new FileWriter(new File("file.txt"));

2.3 處理流

處理流I/O類名由對已有流封裝的功能 + 抽象流類型組成,常見功能有:

  • 緩衝:對節點流讀寫的數據提供了緩衝的功能,數據可以基於緩衝批量讀寫,提高效率。常見有BufferedInputStream、BufferedOutputStream
  • 字節流轉換為字符流:由InputStreamReader、OutputStreamWriter實現
  • 字節流與基本類型數據相互轉換:這裏基本數據類型數據如int、long、short,由DataInputStream、DataOutputStream實現
  • 字節流與對象實例相互轉換:用於實現對象序列化,由ObjectInputStream、ObjectOutputStream實現

處理流的應用了適配器/裝飾模式,轉換/擴展已有流,處理流的創建通常是在構造函數傳入已有的節點流或處理流:

FileOutputStream fileOutputStream = new FileOutputStream("file.txt");
// 擴展提供緩衝寫
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(fileOutputStream);
 // 擴展提供提供基本數據類型寫
DataOutputStream out = new DataOutputStream(bufferedOutputStream);

3 Java NIO

3.1 標準I/O存在問題

Java NIO(New I/O)是一個可以替代標準Java I/O API的IO API(從Java 1.4開始),Java NIO提供了與標準I/O不同的I/O工作方式,目的是為了解決標準 I/O存在的以下問題:

  • (1) 數據多次拷貝

標準I/O處理,完成一次完整的數據讀寫,至少需要從底層硬件讀到內核空間,再讀到用戶文件,又從用戶空間寫入內核空間,再寫入底層硬件

此外,底層通過write、read等函數進行I/O系統調用時,需要傳入數據所在緩衝區起始地址和長度
由於JVM GC的存在,導致對象在堆中的位置往往會發生移動,移動後傳入系統函數的地址參數就不是真正的緩衝區地址了

可能導致讀寫出錯,為了解決上面的問題,使用標準I/O進行系統調用時,還會額外導致一次數據拷貝:把數據從JVM的堆內拷貝到堆外的連續空間內存(堆外內存)

所以總共經歷6次數據拷貝,執行效率較低

  • (2) 操作阻塞

傳統的網絡I/O處理中,由於請求建立連接(connect),讀取網絡I/O數據(read),發送數據(send)等操作是線程阻塞的

// 等待連接
Socket socket = serverSocket.accept();

// 連接已建立,讀取請求消息
StringBuilder req = new StringBuilder();
byte[] recvByteBuf = new byte[1024];
int len;
while ((len = socket.getInputStream().read(recvByteBuf)) != -1) {
    req.append(new String(recvByteBuf, 0, len, StandardCharsets.UTF_8));
}

// 寫入返回消息
socket.getOutputStream().write(("server response msg".getBytes()));
socket.shutdownOutput();

以上面服務端程序為例,當請求連接已建立,讀取請求消息,服務端調用read方法時,客戶端數據可能還沒就緒(例如客戶端數據還在寫入中或者傳輸中),線程需要在read方法阻塞等待直到數據就緒

為了實現服務端併發響應,每個連接需要獨立的線程單獨處理,當併發請求量大時為了維護連接,內存、線程切換開銷過大

3.2 Buffer

Java NIO核心三大核心組件是Buffer(緩衝區)、Channel(通道)、Selector

Buffer提供了常用於I/O操作的字節緩衝區,常見的緩存區有ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分別對應基本數據類型: byte, char, double, float, int, long, short,下面介紹主要以最常用的ByteBuffer為例,Buffer底層支持Java堆內(HeapByteBuffer)或堆外內存(DirectByteBuffer)

堆外內存是指與堆內存相對應的,把內存對象分配在JVM堆以外的內存,這些內存直接受操作系統管理(而不是虛擬機,相比堆內內存,I/O操作中使用堆外內存的優勢在於:

  • 不用被JVM GC線回收,減少GC線程資源佔有
  • 在I/O系統調用時,直接操作堆外內存,可以節省一次堆外內存和堆內內存的複製

ByteBuffer底層堆外內存的分配和釋放基於malloc和free函數,對外allocateDirect方法可以申請分配堆外內存,並返回繼承ByteBuffer類的DirectByteBuffer對象:

public static ByteBuffer allocateDirect(int capacity) {
    return new DirectByteBuffer(capacity);
}

堆外內存的回收基於DirectByteBuffer的成員變量Cleaner類,提供clean方法可以用於主動回收,Netty中大部分堆外內存通過記錄定位Cleaner的存在,主動調用clean方法來回收;
另外,當DirectByteBuffer對象被GC時,關聯的堆外內存也會被回收

tips: JVM參數不建議設置-XX:+DisableExplicitGC,因為部分依賴Java NIO的框架(例如Netty)在內存異常耗盡時,會主動調用System.gc(),觸發Full GC,回收DirectByteBuffer對象,作為回收堆外內存的最後保障機制,設置該參數之後會導致在該情況下堆外內存得不到清理

堆外內存基於基礎ByteBuffer類的DirectByteBuffer類成員變量:Cleaner對象,這個Cleaner對象會在合適的時候執行unsafe.freeMemory(address),從而回收這塊堆外內存

Buffer可以見到理解為一組基本數據類型,存儲地址連續的的數組,支持讀寫操作,對應讀模式和寫模式,通過幾個變量來保存這個數據的當前位置狀態:capacity、 position、 limit:

  • capacity 緩衝區數組的總長度
  • position 下一個要操作的數據元素的位置
  • limit 緩衝區數組中不可操作的下一個元素的位置:limit <= capacity

3.3 Channel

Channel(通道)的概念可以類比I/O流對象,NIO中I/O操作主要基於Channel:
從Channel進行數據讀取 :創建一個緩衝區,然後請求Channel讀取數據
從Channel進行數據寫入 :創建一個緩衝區,填充數據,請求Channel寫入數據

Channel和流非常相似,主要有以下幾點區別:

  • Channel可以讀和寫,而標準I/O流是單向的
  • Channel可以異步讀寫,標準I/O流需要線程阻塞等待直到讀寫操作完成
  • Channel總是基於緩衝區Buffer讀寫

Java NIO中最重要的幾個Channel的實現:

  • FileChannel: 用於文件的數據讀寫,基於FileChannel提供的方法能減少讀寫文件數據拷貝次數,後面會介紹
  • DatagramChannel: 用於UDP的數據讀寫
  • SocketChannel: 用於TCP的數據讀寫,代表客戶端連接
  • ServerSocketChannel: 監聽TCP連接請求,每個請求會創建會一個SocketChannel,一般用於服務端

基於標準I/O中,我們第一步可能要像下面這樣獲取輸入流,按字節把磁盤上的數據讀取到程序中,再進行下一步操作,而在NIO編程中,需要先獲取Channel,再進行讀寫

FileInputStream fileInputStream = new FileInputStream("test.txt");
FileChannel channel = fileInputStream.channel();

tips: FileChannel僅能運行在阻塞模式下,文件異步處理的 I/O 是在JDK 1.7 才被加入的 java.nio.channels.AsynchronousFileChannel

// server socket channel:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(InetAddress.getLocalHost(), 9091));

while (true) {
    SocketChannel socketChannel = serverSocketChannel.accept();
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    int readBytes = socketChannel.read(buffer);
    if (readBytes > 0) {
        // 從寫數據到buffer翻轉為從buffer讀數據
        buffer.flip();
        byte[] bytes = new byte[buffer.remaining()];
        buffer.get(bytes);
        String body = new String(bytes, StandardCharsets.UTF_8);
        System.out.println("server 收到:" + body);
    }
}

3.4 Selector

Selector(選擇器) ,它是Java NIO核心組件中的一個,用於檢查一個或多個NIO Channel(通道)的狀態是否處於可讀、可寫。實現單線程管理多個Channel,也就是可以管理多個網絡連接

Selector核心在於基於操作系統提供的I/O復用功能,單個線程可以同時監視多個連接描述符,一旦某個連接就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作,常見有select、poll、epoll等不同實現

Java NIO Selector基本工作原理如下:

  • (1) 初始化Selector對象,服務端ServerSocketChannel對象
  • (2) 向Selector註冊ServerSocketChannel的socket-accept事件
  • (3) 線程阻塞於selector.select(),當有客戶端請求服務端,線程退出阻塞
  • (4) 基於selector獲取所有就緒事件,此時先獲取到socket-accept事件,向Selector註冊客戶端SocketChannel的數據就緒可讀事件事件
  • (5) 線程再次阻塞於selector.select(),當有客戶端連接數據就緒,可讀
  • (6) 基於ByteBuffer讀取客戶端請求數據,然後寫入響應數據,關閉channel

示例如下,完整可運行代碼已經上傳github(https://github.com/caison/caison-blog-demo):

Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9091));
// 配置通道為非阻塞模式
serverSocketChannel.configureBlocking(false);
// 註冊服務端的socket-accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
    // selector.select()會一直阻塞,直到有channel相關操作就緒
    selector.select();
    // SelectionKey關聯的channel都有就緒事件
    Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();

    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        // 服務端socket-accept
        if (key.isAcceptable()) {
            // 獲取客戶端連接的channel
            SocketChannel clientSocketChannel = serverSocketChannel.accept();
            // 設置為非阻塞模式
            clientSocketChannel.configureBlocking(false);
            // 註冊監聽該客戶端channel可讀事件,併為channel關聯新分配的buffer
            clientSocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
        }

        // channel可讀
        if (key.isReadable()) {
            SocketChannel socketChannel = (SocketChannel) key.channel();
            ByteBuffer buf = (ByteBuffer) key.attachment();

            int bytesRead;
            StringBuilder reqMsg = new StringBuilder();
            while ((bytesRead = socketChannel.read(buf)) > 0) {
                // 從buf寫模式切換為讀模式
                buf.flip();
                int bufRemain = buf.remaining();
                byte[] bytes = new byte[bufRemain];
                buf.get(bytes, 0, bytesRead);
                // 這裏當數據包大於byteBuffer長度,有可能有粘包/拆包問題
                reqMsg.append(new String(bytes, StandardCharsets.UTF_8));
                buf.clear();
            }
            System.out.println("服務端收到報文:" + reqMsg.toString());
            if (bytesRead == -1) {
                byte[] bytes = "[這是服務回的報文的報文]".getBytes(StandardCharsets.UTF_8);

                int length;
                for (int offset = 0; offset < bytes.length; offset += length) {
                    length = Math.min(buf.capacity(), bytes.length - offset);
                    buf.clear();
                    buf.put(bytes, offset, length);
                    buf.flip();
                    socketChannel.write(buf);
                }
                socketChannel.close();
            }
        }
        // Selector不會自己從已selectedKeys中移除SelectionKey實例
        // 必須在處理完通道時自己移除 下次該channel變成就緒時,Selector會再次將其放入selectedKeys中
        keyIterator.remove();
    }
}

tips: Java NIO基於Selector實現高性能網絡I/O這塊使用起來比較繁瑣,使用不友好,一般業界使用基於Java NIO進行封裝優化,擴展豐富功能的Netty框架來優雅實現

高性能I/O優化

下面結合業界熱門開源項目介紹高性能I/O的優化

1 零拷貝

零拷貝(zero copy)技術,用於在數據讀寫中減少甚至完全避免不必要的CPU拷貝,減少內存帶寬的佔用,提高執行效率,零拷貝有幾種不同的實現原理,下面介紹常見開源項目中零拷貝實現

1.1 Kafka零拷貝

Kafka基於Linux 2.1內核提供,並在2.4 內核改進的的sendfile函數 + 硬件提供的DMA Gather Copy實現零拷貝,將文件通過socket傳送

函數通過一次系統調用完成了文件的傳送,減少了原來read/write方式的模式切換。同時減少了數據的copy, sendfile的詳細過程如下:

基本流程如下:

  • (1) 用戶進程發起sendfile系統調用
  • (2) 內核基於DMA Copy將文件數據從磁盤拷貝到內核緩衝區
  • (3) 內核將內核緩衝區中的文件描述信息(文件描述符,數據長度)拷貝到Socket緩衝區
  • (4) 內核基於Socket緩衝區中的文件描述信息和DMA硬件提供的Gather Copy功能將內核緩衝區數據複製到網卡
  • (5) 用戶進程sendfile系統調用完成並返回

相比傳統的I/O方式,sendfile + DMA Gather Copy方式實現的零拷貝,數據拷貝次數從4次降為2次,系統調用從2次降為1次,用戶進程上下文切換次數從4次變成2次DMA Copy,大大提高處理效率

Kafka底層基於java.nio包下的FileChannel的transferTo:

public abstract long transferTo(long position, long count, WritableByteChannel target)

transferTo將FileChannel關聯的文件發送到指定channel,當Comsumer消費數據,Kafka Server基於FileChannel將文件中的消息數據發送到SocketChannel

1.2 RocketMQ零拷貝

RocketMQ基於mmap + write的方式實現零拷貝:
mmap() 可以將內核中緩衝區的地址與用戶空間的緩衝區進行映射,實現數據共享,省去了將數據從內核緩衝區拷貝到用戶緩衝區

tmp_buf = mmap(file, len); 
write(socket, tmp_buf, len);

mmap + write 實現零拷貝的基本流程如下:

  • (1) 用戶進程向內核發起系統mmap調用
  • (2) 將用戶進程的內核空間的讀緩衝區與用戶空間的緩存區進行內存地址映射
  • (3) 內核基於DMA Copy將文件數據從磁盤複製到內核緩衝區
  • (4) 用戶進程mmap系統調用完成並返回
  • (5) 用戶進程向內核發起write系統調用
  • (6) 內核基於CPU Copy將數據從內核緩衝區拷貝到Socket緩衝區
  • (7) 內核基於DMA Copy將數據從Socket緩衝區拷貝到網卡
  • (8) 用戶進程write系統調用完成並返回

RocketMQ中消息基於mmap實現存儲和加載的邏輯寫在org.apache.rocketmq.store.MappedFile中,內部實現基於nio提供的java.nio.MappedByteBuffer,基於FileChannel的map方法得到mmap的緩衝區:

// 初始化
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);

查詢CommitLog的消息時,基於mappedByteBuffer偏移量pos,數據大小size查詢:

public SelectMappedBufferResult selectMappedBuffer(int pos, int size) {
    int readPosition = getReadPosition();
    // ...各種安全校驗
    
    // 返回mappedByteBuffer視圖
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    byteBuffer.position(pos);
    ByteBuffer byteBufferNew = byteBuffer.slice();
    byteBufferNew.limit(size);
    return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);
}

tips: transientStorePoolEnable機制
Java NIO mmap的部分內存並不是常駐內存,可以被置換到交換內存(虛擬內存),RocketMQ為了提高消息發送的性能,引入了內存鎖定機制,即將最近需要操作的CommitLog文件映射到內存,並提供內存鎖定功能,確保這些文件始終存在內存中,該機制的控制參數就是transientStorePoolEnable

因此,MappedFile數據保存CommitLog刷盤有2種方式:

  • 1 開啟transientStorePoolEnable:寫入內存字節緩衝區(writeBuffer) -> 從內存字節緩衝區(writeBuffer)提交(commit)到文件通道(fileChannel) -> 文件通道(fileChannel) -> flush到磁盤
  • 2 未開啟transientStorePoolEnable:寫入映射文件字節緩衝區(mappedByteBuffer) -> 映射文件字節緩衝區(mappedByteBuffer) -> flush到磁盤

RocketMQ 基於 mmap+write 實現零拷貝,適用於業務級消息這種小塊文件的數據持久化和傳輸
Kafka 基於 sendfile 這種零拷貝方式,適用於系統日誌消息這種高吞吐量的大塊文件的數據持久化和傳輸

tips: Kafka 的索引文件使用的是 mmap+write 方式,數據文件發送網絡使用的是 sendfile 方式

1.3 Netty零拷貝

Netty 的零拷貝分為兩種:

  • 1 基於操作系統實現的零拷貝,底層基於FileChannel的transferTo方法
  • 2 基於Java 層操作優化,對數組緩存對象(ByteBuf )進行封裝優化,通過對ByteBuf數據建立數據視圖,支持ByteBuf 對象合併,切分,當底層僅保留一份數據存儲,減少不必要拷貝

2 多路復用

Netty中對Java NIO功能封裝優化之後,實現I/O多路復用代碼優雅了很多:

// 創建mainReactor
NioEventLoopGroup boosGroup = new NioEventLoopGroup();
// 創建工作線程組
NioEventLoopGroup workerGroup = new NioEventLoopGroup();

final ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap 
     // 組裝NioEventLoopGroup 
    .group(boosGroup, workerGroup)
     // 設置channel類型為NIO類型
    .channel(NioServerSocketChannel.class)
    // 設置連接配置參數
    .option(ChannelOption.SO_BACKLOG, 1024)
    .childOption(ChannelOption.SO_KEEPALIVE, true)
    .childOption(ChannelOption.TCP_NODELAY, true)
    // 配置入站、出站事件handler
    .childHandler(new ChannelInitializer<NioSocketChannel>() {
        @Override
        protected void initChannel(NioSocketChannel ch) {
            // 配置入站、出站事件channel
            ch.pipeline().addLast(...);
            ch.pipeline().addLast(...);
        }
    });

// 綁定端口
int port = 8080;
serverBootstrap.bind(port).addListener(future -> {
    if (future.isSuccess()) {
        System.out.println(new Date() + ": 端口[" + port + "]綁定成功!");
    } else {
        System.err.println("端口[" + port + "]綁定失敗!");
    }
});

3 頁緩存(PageCache)

頁緩存(PageCache)是操作系統對文件的緩存,用來減少對磁盤的 I/O 操作,以頁為單位的,內容就是磁盤上的物理塊,頁緩存能幫助程序對文件進行順序讀寫的速度幾乎接近於內存的讀寫速度,主要原因就是由於OS使用PageCache機制對讀寫訪問操作進行了性能優化:

頁緩存讀取策略:當進程發起一個讀操作 (比如,進程發起一個 read() 系統調用),它首先會檢查需要的數據是否在頁緩存中:

  • 如果在,則放棄訪問磁盤,而直接從頁緩存中讀取
  • 如果不在,則內核調度塊 I/O 操作從磁盤去讀取數據,並讀入緊隨其後的少數幾個頁面(不少於一個頁面,通常是三個頁面),然後將數據放入頁緩存中

頁緩存寫策略:當進程發起write系統調用寫數據到文件中,先寫到頁緩存,然後方法返回。此時數據還沒有真正的保存到文件中去,Linux 僅僅將頁緩存中的這一頁數據標記為“臟”,並且被加入到臟頁鏈表中

然後,由flusher 回寫線程周期性將臟頁鏈表中的頁寫到磁盤,讓磁盤中的數據和內存中保持一致,最後清理“臟”標識。在以下三種情況下,臟頁會被寫回磁盤:

  • 空閑內存低於一個特定閾值
  • 臟頁在內存中駐留超過一個特定的閾值時
  • 當用戶進程調用 sync() 和 fsync() 系統調用時

RocketMQ中,ConsumeQueue邏輯消費隊列存儲的數據較少,並且是順序讀取,在page cache機制的預讀取作用下,Consume Queue文件的讀性能幾乎接近讀內存,即使在有消息堆積情況下也不會影響性能,提供了2種消息刷盤策略:

  • 同步刷盤:在消息真正持久化至磁盤后RocketMQ的Broker端才會真正返回給Producer端一個成功的ACK響應
  • 異步刷盤,能充分利用操作系統的PageCache的優勢,只要消息寫入PageCache即可將成功的ACK返回給Producer端。消息刷盤採用後台異步線程提交的方式進行,降低了讀寫延遲,提高了MQ的性能和吞吐量

Kafka實現消息高性能讀寫也利用了頁緩存,這裏不再展開

參考

《深入理解Linux內核 —— Daniel P.Bovet》

更多精彩,歡迎關注公眾號 分佈式系統架構

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

【其他文章推薦】

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

※評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

※智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

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

※幫你省時又省力,新北清潔一流服務好口碑