使用dotnet Cli向nuget發布包_如何寫文案

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

長話短說, 今天分享如何在nuget.org創建併發布.NET Standard package。

前置

  1. 安裝勾選.NET Core開發套件的Visual Studio; 安裝dotnet Cli

從VS2017開始,dotnet Cli已經自動在.NET開發套件中被安裝;
使用SDK-style format(SDK屬性)的.NET Standard項目需要dotnet Cli;nuget.exe Cli用於非SDK樣式的項目(通常是.NET Framework)。

  1. 創建.NET Standard庫項目

配置package屬性

  1. 項目右鍵,選擇屬性—>打包
    打包面板只會出現在VSSDK-style項目,典型如.NET Standard或者.NET Core庫項目。

構建時生成Nuget包:顧名思義,除打包命令,構建時也會自動生成nuget包;
標記:幫助其他人定位你的包,了解包的能力。

  1. 給package設定一個唯一id,並填寫其他屬性。注意這個package id需要在nuget.org全站唯一,我們建議你使用包前綴名來避免重複,比如: ${UserName}.PackageName

打包

  1. 將配置改為Release
  2. 右鍵項目–> 打包

如果你沒有看到打包命令,你的項目可能不是SDk-style風格的項目,這是要使用
nuget.exe Cli(或者遷移到SDK-style項目,再使用dotnet Cli)。

  1. Visual Studio構建項目並創建.nupkg文件,插看輸出窗體,獲取包文件的路徑。

發布包

有了.nupkg文件,你可以從nuget.org獲取一個API key(這個Key標記了你這個nuget賬戶),結合dotnet cli發布包。

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

獲取API Key

  1. 登陸nuget.org賬戶
  2. 點擊右上角你的賬戶名字,選擇API keys
  3. 創建—> 選擇範圍–> Push, 在Glob pattern填入*
  4. 一旦生成key,Copy並儘早保存key,這個key將在dotnet Cli命令中用到

儘快保存你的key,如果你再次返回這個頁面,你需要重新生成key並Copy.

發布包

轉到包含.nupkg文件的目錄,執行下面命令

dotnet nuget push AppLogger.1.0.0.nupkg -k qz2jga8pl3dvn2akksyquwcs9ygggg4exypy3bhxy6w6x6 -s https://api.nuget.org/v3/index.json

显示如下結果:

info : Pushing AppLogger.1.0.0.nupkg to 'https://www.nuget.org/api/v2/package'...
info :   PUT https://www.nuget.org/api/v2/package/
info :   Created https://www.nuget.org/api/v2/package/ 12620ms
info : Your package was pushed.

推送命令中的錯誤通常指示問題原因,如:

  • 您可能忘記了更新項目中的版本號,而嘗試發布同簽名軟件包。
  • 您嘗試使用主機上已存在的包標記符發布程序包時,也會看到錯誤:名稱“ AppLogger”已經存在。
Response status code does not indicate success: 403 (The specified API key is invalid,
has expired, or does not have permission to access the specified package.).

如果您確認使用的是有效API key,則此錯誤提示不準確,實際錯誤是命名衝突
更改程序包標識符,重建項目,重新產生.nupkg文件,然後重試push命令。
以上便是向nuget發布包的主要過程。

其他一些包管理的操作,請在nuget包管理中心倒騰

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

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

Google Store 推出新年優惠,Pixel 手機、Chromecast、Nest 智慧喇叭直接現折_如何寫文案

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

隨著新年即將來臨,Google Store 近日也宣布推出新年優惠活動,Pixel 手機、Chromecast、Nest 智慧喇叭系列都能祭出折扣,而且這次是直接現折,讓你立刻就能享受優惠價。此外,Pixel 4a 5G 版本的白色款式也已悄悄降臨 Google Store(不過沒有折扣)。

Google Store 推出新年優惠

這次符合 Google Store 新年優惠的 Pixel 手機,只有 Pixel 4a 5G 版本(純粹黑),原價 NT$15,990,現在入手即可省下 NT$1,000,意味著價格變成 NT$14,990。這次也是首次 Pixel 4a 5G 推出現折優惠的活動(之前送折價卷)。就是白因為才剛推出,理所當然還是以原價販售:

Pixel 5 這次就不在名單內,目前依舊以 NT$18,990 的原價販售。而 Nest 智慧喇叭「Nest Mini」與「Nest Audio」兩款都有優惠。

Nest Mini 原價為 NT$1,785,現在現省 NT$400,變成 NT$1,385 的優惠價,兩色都是:

Nest Audio 原價為 NT$3,180,現在現省 NT$350,折扣後價格變成 NT$2,830:

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

Chromecast 折扣 NT$150,原價 NT$1,445,現在購買可以 NT$1,295 的優惠價入手:

整體來看,這次 Google Store 的優惠雖然沒有像先前聖誕節、雙11 那麼超值,不過跟平常相比至少有降價,對於有意購買上述產品的人來說,還是可以省下一些荷包,剛好拿來補貼紅包錢。

Google Store 新年優惠預計到 2021 年 2 月 14 日晚上 11:59 點截止。

補充資料

  • 台灣 Google Store:點我前往

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

vue的第一個commit分析_如何寫文案

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

什麼是銷售文案服務?A就是幫你撰寫適合的廣告文案。當您需要販售商品、宣傳活動、建立個人品牌,撰寫廣告文案都是必須的工作。

為什麼寫這篇vue的分析文章?

對於天資愚鈍的前端(我)來說,閱讀源碼是件不容易的事情,畢竟有時候看源碼分析的文章都看不懂。每次看到大佬們用了1~2年的vue就能掌握原理,甚至精通源碼,再看看自己用了好幾年都還在基本的使用階段,心中總是羞愧不已。如果一直滿足於基本的業務開發,怕是得在初級水平一直待下去了吧。所以希望在學習源碼的同時記錄知識點,可以讓自己的理解和記憶更加深刻,也方便將來查閱。

目錄結構

本文以vue的第一次 commit a879ec06 作為分析版本

├── build
│   └── build.js               // `rollup` 打包配置
├── dist                        
│   └── vue.js    
├── package.json
├── src                        // vue源碼目錄
│   ├── compiler               // 將vue-template轉化為render函數
│   │   ├── codegen.js         // 遞歸ast提取指令,分類attr,style,class,並生成render函數
│   │   ├── html-parser.js     // 通過正則匹配將html字符串轉化為ast
│   │   ├── index.js           // compile主入口
│   │   └── text-parser.js     // 編譯{{}}
│   ├── config.js              // 對於vue的全局配置文件
│   ├── index.js               // 主入口
│   ├── index.umd.js           // 未知(應該是umd格式的主入口)
│   ├── instance               // vue實例函數
│   │   └── index.js           // 包含了vue實例的初始化,compile,data代理,methods代理,watch數據,執行渲染
│   ├── observer               // 數據訂閱發布的實現
│   │   ├── array.js           // 實現array變異方法,$set $remove 實現
│   │   ├── batcher.js         // watch執行隊列的收集,執行
│   │   ├── dep.js             // 訂閱中心實現
│   │   ├── index.js           // 數據劫持的實現,收集訂閱者
│   │   └── watcher.js         // watch實現,訂閱者
│   ├── util                   // 工具函數
│   │   ├── component.js
│   │   ├── debug.js
│   │   ├── dom.js
│   │   ├── env.js             // nexttick實現
│   │   ├── index.js
│   │   ├── lang.js
│   │   └── options.js
│   └── vdom
│       ├── dom.js             // dom操作的封裝
│       ├── h.js               // 節點數據分析(元素節點,文本節點)
│       ├── index.js           // vdom主入口
│       ├── modules            // 不同屬性處理函數
│       │   ├── attrs.js       // 普通attr屬性處理
│       │   ├── class.js       // class處理
│       │   ├── events.js      // event處理
│       │   ├── props.js       // props處理
│       │   └── style.js       // style處理
│       ├── patch.js           // node樹的渲染,包括節點的加減更新處理,及對應attr的處理
│       └── vnode.js           // 返回最終的節點數據
└── webpack.config.js          // webpack配置

從template到html的過程分析

我們的代碼是從new Vue()開始的,Vue的構造函數如下:

constructor (options) {
  // options就是我們對於vue的配置
  this.$options = options
  this._data = options.data
  // 獲取元素html,即template
  const el = this._el = document.querySelector(options.el)
  // 編譯模板 -> render函數
  const render = compile(getOuterHTML(el))
  this._el.innerHTML = ''
  // 實例代理data數據
  Object.keys(options.data).forEach(key => this._proxy(key))
  // 將method的this指向實例
  if (options.methods) {
    Object.keys(options.methods).forEach(key => {
      this[key] = options.methods[key].bind(this)
    })
  }
  // 數據觀察
  this._ob = observe(options.data)
  this._watchers = []
  // watch數據及更新
  this._watcher = new Watcher(this, render, this._update)
  // 渲染函數
  this._update(this._watcher.value)
}

當我們初始化項目的時候,即會執行構造函數,該函數向我們展示了vue初始化的主線:編譯template字符串 => 代理data數據/methods的this綁定 => 數據觀察 => 建立watch及更新渲染

1. 編譯template字符串

const render = compile(getOuterHTML(el))

其中compile的實現如下:

export function compile (html) {
  html = html.trim()
  // 對編譯結果緩存
  const hit = cache[html]
  // parse函數在parse-html中定義,其作用是把我們獲取的html字符串通過正則匹配轉化為ast,輸出如下 {tag: 'div', attrs: {}, children: []}
  return hit || (cache[html] = generate(parse(html)))
}

接下來看看generate函數,ast通過genElement的轉化生成了構建節點html的函數,在genElement將對if for 等進行判斷並轉化( 指令的具體處理將在後面做分析,先關注主流程代碼),最後都會執行genData函數

// 生成節點主函數
export function generate (ast) {
  const code = genElement(ast)
  // 執行code代碼,並將this作為code的global對象。所以我們在template中的變量將指向為實例的屬性 {{name}} -> this.name 
  return new Function (`with (this) { return ${code}}`)
}

// 解析單個節點 -> genData
function genElement (el, key) {
  let exp
  // 指令的實現,實際就是在模板編譯時實現的
  if (exp = getAttr(el, 'v-for')) {
    return genFor(el, exp)
  } else if (exp = getAttr(el, 'v-if')) {
    return genIf(el, exp)
  } else if (el.tag === 'template') {
    return genChildren(el)
  } else {
    // 分別為 tag 自身屬性 子節點數據
    return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
  }
}

我們可以看看在genData中都做了什麼。上面的parse函數將html字符串轉化為ast,而在genData中則將節點的attrs數據進一步處理,例如class -> renderClass style class props attr 分類。在這裏可以看到 bind 指令的實現,即通過正則匹配 : 和 bind,如果匹配則把相應的 value值轉化為 (value) 的形式,而不匹配的則通過JSON.stringify()轉化為字符串('value')。最後輸出attrs(key-value),在這裏得到的對象是字符串形式的,例如(value)等也僅僅是將變量名,而在generate中通過new Function進一步通過(this.value)得到變量值。

function genData (el, key) {
  // 沒有屬性返回空對象
  if (!el.attrs.length) {
    return '{}'
  }
  // key
  let data = key ? `{key:${ key },` : `{`
  // class處理
  if (el.attrsMap[':class'] || el.attrsMap['class']) {
    data += `class: _renderClass(${ el.attrsMap[':class'] }, "${ el.attrsMap['class'] || '' }"),`
  }
  // attrs
  let attrs = `attrs:{`
  let props = `props:{`
  let hasAttrs = false
  let hasProps = false
  for (let i = 0, l = el.attrs.length; i < l; i++) {
    let attr = el.attrs[i]
    let name = attr.name
    // bind屬性
    if (bindRE.test(name)) {
      name = name.replace(bindRE, '')
      if (name === 'class') {
        continue
      // style處理
      } else if (name === 'style') {
        data += `style: ${ attr.value },`
      // props屬性處理
      } else if (mustUsePropsRE.test(name)) {
        hasProps = true
        props += `"${ name }": (${ attr.value }),` 
      // 其他屬性
      } else {
        hasAttrs = true
        attrs += `"${ name }": (${ attr.value }),`
      }
    // on指令,未實現
    } else if (onRE.test(name)) {
      name = name.replace(onRE, '')
    // 普通屬性
    } else if (name !== 'class') {
      hasAttrs = true
      attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
    }
  }
  if (hasAttrs) {
    data += attrs.slice(0, -1) + '},'
  }
  if (hasProps) {
    data += props.slice(0, -1) + '},'
  }
  return data.replace(/,$/, '') + '}'
}

而對於genChildren,我們可以猜到就是對ast中的children進行遍歷調用genElement,實際上在這裏還包括了對文本節點的處理。

// 遍歷子節點 -> genNode
function genChildren (el) {
  if (!el.children.length) {
    return 'undefined'
  }
  // 對children扁平化處理
  return '__flatten__([' + el.children.map(genNode).join(',') + '])'
}

function genNode (node) {
  if (node.tag) {
    return genElement(node)
  } else {
    return genText(node)
  }
}

// 解析{{}}
function genText (text) {
  if (text === ' ') {
    return '" "'
  } else {
    const exp = parseText(text)
    if (exp) {
      return 'String(' + escapeNewlines(exp) + ')'
    } else {
      return escapeNewlines(JSON.stringify(text))
    }
  }
}

genText處理了text及換行,在parseText函數中利用正則解析{{}},輸出字符串(value)形式的字符串。

現在我們再看看__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })__h__函數

// h 函數利用上面得到的節點數據得到 vNode對象 => 虛擬dom
export default function h (tag, b, c) {
  var data = {}, children, text, i
  if (arguments.length === 3) {
    data = b
    if (isArray(c)) { children = c }
    else if (isPrimitive(c)) { text = c }
  } else if (arguments.length === 2) {
    if (isArray(b)) { children = b }
    else if (isPrimitive(b)) { text = b }
    else { data = b }
  }
  if (isArray(children)) {
    // 子節點遞歸處理
    for (i = 0; i < children.length; ++i) {
      if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
    }
  }
  // svg處理
  if (tag === 'svg') {
    addNS(data, children)
  }
  // 子節點為文本節點
  return VNode(tag, data, children, text, undefined)
}

到此為止,我們分析了const render = compile(getOuterHTML(el)),從elhtml字符串到render函數都是怎麼處理的。

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

銷售文案是什麼?A文案是廣告用的文字。舉凡任何宣傳、行銷、販賣商品時所用到的文字都是文案。在網路時代,文案成為行銷中最重要的宣傳方式,好的文案可節省大量宣傳資源,達成行銷目的。

2. 代理data數據/methods的this綁定

// 實例代理data數據
Object.keys(options.data).forEach(key => this._proxy(key))
// 將method的this指向實例
if (options.methods) {
  Object.keys(options.methods).forEach(key => {
    this[key] = options.methods[key].bind(this)
  })
}

實例代理data數據的實現比較簡單,就是利用了對象的setter和getter,讀取this數據時返回data數據,在設置this數據時同步設置data數據

_proxy (key) {
  if (!isReserved(key)) {
    // need to store ref to self here
    // because these getter/setters might
    // be called by child scopes via
    // prototype inheritance.
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter () {
        return self._data[key]
      },
      set: function proxySetter (val) {
        self._data[key] = val
      }
    })
  }
}

3. Obaerve的實現

Observe的實現原理在很多地方都有分析,主要是利用了Object.defineProperty()來建立對數據更改的訂閱,在很多地方也稱之為數據劫持。下面我們來學習從零開始建立這樣一個數據的訂閱發布體系。

從簡單處開始,我們希望有個函數可以幫我們監聽數據的改變,每當數據改變時執行特定回調函數

function observe(data, callback) {
  if (!data || typeof data !== 'object') {
    return
  }

  // 遍歷key
  Object.keys(data).forEach((key) => {
    let value = data[key];

    // 遞歸遍歷監聽深度變化
    observe(value, callback);

    // 監聽單個可以的變化
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        return value;
      },
      set(val) {
        if (val === value) {
          return
        }

        value = val;

        // 監聽新的數據
        observe(value, callback);
        
        // 數據改變的回調
        callback();
      }
    });
  });
}

// 使用observe函數監聽data
const data = {};
observe(data, () => {
  console.log('data修改');
})

上面我們實現了一個簡單的observe函數,只要我們將編譯函數作為callback傳入,那麼每次數據更改時都會觸發回調函數。但是我們現在不能為單獨的key設置監聽及回調函數,只能監聽整個對象的變化執行回調。下面我們對函數進行改進,達到為某個key設置監聽及回調。同時建立調度中心,讓整個訂閱發布模式更加清晰。

// 首先是訂閱中心
class Dep {
  constructor() {
    this.subs = []; // 訂閱者數組
  }

  addSub(sub) {
    // 添加訂閱者
    this.subs.push(sub);
  }

  notify() {
    // 發布通知
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

// 當前訂閱者,在getter中標記
Dep.target = null;

// 訂閱者
class Watch {
  constructor(express, cb) {
    this.cb = cb;
    if (typeof express === 'function') {
      this.expressFn = express;
    } else {
      this.expressFn = () => {
        return new Function(express)();
      }
    }
    
    this.get();
  }

  get() {
    // 利用Dep.target存當前訂閱者
    Dep.target = this;
    // 執行表達式 -> 觸發getter -> 在getter中添加訂閱者
    this.expressFn();
    // 及時置空
    Dep.taget = null;
  }

  update() {
    // 更新
    this.cb();
  }

  addDep(dep) {
    // 添加訂閱
    dep.addSub(this);
  }
}

// 觀察者 建立觀察
class Observe {
  constructor(data) {
    if (!data || typeof data !== 'object') {
      return
    }
  
    // 遍歷key
    Object.keys(data).forEach((key) => {
      // key => dep 對應
      const dep = new Dep();
      let value = data[key];
  
      // 遞歸遍歷監聽深度變化
      const observe = new Observe(value);
  
      // 監聽單個可以的變化
      Object.defineProperty(data, key, {
        configurable: true,
        enumerable: true,
        get() {
          if (Dep.target) {
            const watch = Dep.target;
            watch.addDep(dep);
          }
          return value;
        },
        set(val) {
          if (val === value) {
            return
          }
  
          value = val;
  
          // 監聽新的數據
          new Observe(value);
          
          // 數據改變的回調
          dep.notify();
        }
      });
    });
  }
}

// 監聽數據中某個key的更改
const data = {
  name: 'xiaoming',
  age: 26
};

const observe = new Observe(data);

const watch = new Watch('data.age', () => {
  console.log('age update');
});

data.age = 22

現在我們實現了訂閱中心訂閱者觀察者。觀察者監測數據的更新,訂閱者通過訂閱中心訂閱數據的更新,當數據更新時,觀察者會告訴訂閱中心,訂閱中心再逐個通知所有的訂閱者執行更新函數。到現在為止,我們可以大概猜出vue的實現原理:

  1. 建立觀察者觀察data數據的更改 (new Observe)

  2. 在編譯的時候,當某個代碼片段或節點依賴data數據,為該節點建議訂閱者,訂閱data中某些數據的更新(new Watch)

  3. 當dada數據更新時,通過訂閱中心通知數據更新,執行節點更新函數,新建或更新節點(dep.notify())

上面是我們對vue實現原理訂閱發布模式的基本實現,及編譯到更新過程的猜想,現在我們接着分析vue源碼的實現:

在實例的初始化中

// ...
// 為數據建立數據觀察
this._ob = observe(options.data)
this._watchers = []
// 添加訂閱者 執行render 會觸發 getter 訂閱者訂閱更新,數據改變觸發 setter 訂閱中心通知訂閱者執行 update
this._watcher = new Watcher(this, render, this._update)
// ...

vue中數據觀察的實現

// observe函數
export function observe (value, vm) {
  if (!value || typeof value !== 'object') {
    return
  }
  if (
    hasOwn(value, '__ob__') &&
    value.__ob__ instanceof Observer
  ) {
    ob = value.__ob__
  } else if (
    shouldConvert &&
    (isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    // 為數據建立觀察者
    ob = new Observer(value)
  }
  // 存儲關聯的vm
  if (ob && vm) {
    ob.addVm(vm)
  }
  return ob
}

// => Observe 函數
export function Observer (value) {
  this.value = value
  // 在數組變異方法中有用
  this.dep = new Dep()
  // observer實例存在__ob__中
  def(value, '__ob__', this)
  if (isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment
    // 數組遍歷,添加變異的數組方法
    augment(value, arrayMethods, arrayKeys)
    // 對數組的每個選項調用observe函數
    this.observeArray(value)
  } else {
    // walk -> convert -> defineReactive -> setter/getter
    this.walk(value)
  }
}

// => walk
Observer.prototype.walk = function (obj) {
  var keys = Object.keys(obj)
  for (var i = 0, l = keys.length; i < l; i++) {
    this.convert(keys[i], obj[keys[i]])
  }
}

// => convert
Observer.prototype.convert = function (key, val) {
  defineReactive(this.value, key, val)
}

// 重點看看defineReactive
export function defineReactive (obj, key, val) {
  // key對應的的訂閱中心
  var dep = new Dep()

  var property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // 兼容原有setter/getter
  // cater for pre-defined getter/setters
  var getter = property && property.get
  var setter = property && property.set

  // 實現遞歸監聽屬性 val = obj[key]
  // 深度優先遍歷 先為子屬性設置 reactive
  var childOb = observe(val)
  // 設置 getter/setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val
      // Dep.target 為當前 watch 實例
      if (Dep.target) {
        // dep 為 obj[key] 對應的調度中心 dep.depend 將當前 wtcher 實例添加到調度中心
        dep.depend()
        if (childOb) {
          // childOb.dep 為 obj[key] 值 val 對應的 observer 實例的 dep
          // 實現array的變異方法和$set方法訂閱
          childOb.dep.depend()
        }

        // TODO: 此處作用未知?
        if (isArray(value)) {
          for (var e, i = 0, l = value.length; i < l; i++) {
            e = value[i]
            e && e.__ob__ && e.__ob__.dep.depend()
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val
      // 通過 getter 獲取 val 判斷是否改變
      if (newVal === value) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 為新值設置 reactive
      childOb = observe(newVal)
      // 通知key對應的訂閱中心更新
      dep.notify()
    }
  })
}

訂閱中心的實現

let uid = 0

export default function Dep () {
  this.id = uid++
  // 訂閱調度中心的watch數組
  this.subs = []
}

// 當前watch實例
Dep.target = null

// 添加訂閱者
Dep.prototype.addSub = function (sub) {
  this.subs.push(sub)
}

// 移除訂閱者
Dep.prototype.removeSub = function (sub) {
  this.subs.$remove(sub)
}

// 訂閱
Dep.prototype.depend = function () {
  // Dep.target.addDep(this) => this.addSub(Dep.target) => this.subs.push(Dep.target)
  Dep.target.addDep(this)
}

// 通知更新
Dep.prototype.notify = function () {
  // stablize the subscriber list first
  var subs = this.subs.slice()
  for (var i = 0, l = subs.length; i < l; i++) {
    // subs[i].update() => watch.update()
    subs[i].update()
  }
}

訂閱者的實現

export default function Watcher (vm, expOrFn, cb, options) {
  // mix in options
  if (options) {
    extend(this, options)
  }
  var isFn = typeof expOrFn === 'function'
  this.vm = vm
  // vm 的 _watchers 包含了所有 watch
  vm._watchers.push(this)
  this.expression = expOrFn
  this.cb = cb
  this.id = ++uid // uid for batching
  this.active = true
  this.dirty = this.lazy // for lazy watchers
  // deps 一個 watch 實例可以對應多個 dep
  this.deps = []
  this.newDeps = []
  this.depIds = Object.create(null)
  this.newDepIds = null
  this.prevError = null // for async error stacks
  // parse expression for getter/setter
  if (isFn) {
    this.getter = expOrFn
    this.setter = undefined
  } else {
    warn('vue-lite only supports watching functions.')
  }
  this.value = this.lazy
    ? undefined
    : this.get()
  this.queued = this.shallow = false
}

Watcher.prototype.get = function () {
  this.beforeGet()
  var scope = this.scope || this.vm
  var value
  try {
    // 執行 expOrFn,此時會觸發 getter => dep.depend() 將watch實例添加到對應 obj[key] 的 dep
    value = this.getter.call(scope, scope)
  }
  if (this.deep) {
    // 深度watch
    // 觸發每個key的getter watch實例將對應多個dep
    traverse(value)
  }
  // ...
  this.afterGet()
  return value
}

// 觸發getter,實現訂閱
Watcher.prototype.beforeGet = function () {
  Dep.target = this
  this.newDepIds = Object.create(null)
  this.newDeps.length = 0
}

// 添加訂閱
Watcher.prototype.addDep = function (dep) {
  var id = dep.id
  if (!this.newDepIds[id]) {
    // 將新出現的dep添加到newDeps中
    this.newDepIds[id] = true
    this.newDeps.push(dep)
    // 如果已在調度中心,不再重複添加
    if (!this.depIds[id]) {
      // 將watch添加到調度中心的數組中
      dep.addSub(this)
    }
  }
}

Watcher.prototype.afterGet = function () {
  // 切除key的getter聯繫
  Dep.target = null
  var i = this.deps.length
  while (i--) {
    var dep = this.deps[i]
    if (!this.newDepIds[dep.id]) {
      // 移除不在expOrFn表達式中關聯的dep中watch的訂閱
      dep.removeSub(this)
    }
  }
  this.depIds = this.newDepIds
  var tmp = this.deps
  this.deps = this.newDeps
  // TODO: 既然newDeps最終會被置空,這邊賦值的意義在於?
  this.newDeps = tmp
}

// 訂閱中心通知消息更新
Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync || !config.async) {
    this.run()
  } else {
    // if queued, only overwrite shallow with non-shallow,
    // but not the other way around.
    this.shallow = this.queued
      ? shallow
        ? this.shallow
        : false
      : !!shallow
    this.queued = true
    // record before-push error stack in debug mode
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.debug) {
      this.prevError = new Error('[vue] async stack trace')
    }
    // 添加到待執行池
    pushWatcher(this)
  }
}

// 執行更新回調
Watcher.prototype.run = function () {
  if (this.active) {
    var value = this.get()
    if (
      ((isObject(value) || this.deep) && !this.shallow)
    ) {
      // set new value
      var oldValue = this.value
      this.value = value
      var prevError = this.prevError
      // ...
      this.cb.call(this.vm, value, oldValue)
    }
    this.queued = this.shallow = false
  }
}

Watcher.prototype.depend = function () {
  var i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

wtach回調執行隊列

在上面我們可以發現,watch在收到信息更新執行update時。如果非同步情況下會執行pushWatcher(this)將實例推入執行池中,那麼在何時會執行回調函數,如何執行呢?我們一起看看pushWatcher的實現。

// batch.js
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false

// 重置執行池
function resetBatcherState () {
  queue = []
  userQueue = []
  // has 避免重複
  has = {}
  circular = {}
  waiting = internalQueueDepleted = false
}

// 執行執行隊列
function flushBatcherQueue () {
  runBatcherQueue(queue)
  internalQueueDepleted = true
  runBatcherQueue(userQueue)
  resetBatcherState()
}

// 批量執行
function runBatcherQueue (queue) {
  for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
    var watcher = queue[queueIndex]
    var id = watcher.id
    // 執行後置為null
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > config._maxUpdateCount) {
        warn(
          'You may have an infinite update loop for watcher ' +
          'with expression "' + watcher.expression + '"',
          watcher.vm
        )
        break
      }
    }
  }
}

// 添加到執行池
export function pushWatcher (watcher) {
  var id = watcher.id
  if (has[id] == null) {
    if (internalQueueDepleted && !watcher.user) {
      // an internal watcher triggered by a user watcher...
      // let's run it immediately after current user watcher is done.
      userQueue.splice(queueIndex + 1, 0, watcher)
    } else {
      // push watcher into appropriate queue
      var q = watcher.user
        ? userQueue
        : queue
      has[id] = q.length
      q.push(watcher)
      // queue the flush
      if (!waiting) {
        waiting = true
        // 在nextick中執行
        nextTick(flushBatcherQueue)
      }
    }
  }
}

4. patch實現

上面便是vue中數據驅動的實現原理,下面我們接着回到主流程中,在執行完watch后,便執行this._update(this._watcher.value)開始節點渲染

// _update => createPatchFunction => patch => patchVnode => (dom api)

// vtree是通過compile函數編譯的render函數執行的結果,返回了當前表示當前dom結構的對象(虛擬節點樹)
_update (vtree) {
  if (!this._tree) {
    // 第一次渲染
    patch(this._el, vtree)
  } else {
    patch(this._tree, vtree)
  }
  this._tree = vtree
}

// 在處理節點時,需要針對class,props,style,attrs,events做不同處理
// 在這裏注入針對不同屬性的處理函數
const patch = createPatchFunction([
  _class, // makes it easy to toggle classes
  props,
  style,
  attrs,
  events
])

// => createPatchFunction返回patch函數,patch函數通過對比虛擬節點的差異,對節點進行增刪更新
// 最後調用原生的dom api更新html
return function patch (oldVnode, vnode) {
  var i, elm, parent
  var insertedVnodeQueue = []
  // pre hook
  for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

  if (isUndef(oldVnode.sel)) {
    oldVnode = emptyNodeAt(oldVnode)
  }

  if (sameVnode(oldVnode, vnode)) {
    // someNode can patch
    patchVnode(oldVnode, vnode, insertedVnodeQueue)
  } else {
    // 正常的不復用 remove insert
    elm = oldVnode.elm
    parent = api.parentNode(elm)

    createElm(vnode, insertedVnodeQueue)

    if (parent !== null) {
      api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
      removeVnodes(parent, [oldVnode], 0, 0)
    }
  }

  for (i = 0; i < insertedVnodeQueue.length; ++i) {
    insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
  }

  // hook post
  for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
  return vnode
}

結尾

以上分析了vue從template 到節點渲染的大致實現,當然也有某些地方沒有全面分析的地方,其中template解析為ast主要通過正則匹配實現,及節點渲染及更新的patch過程主要通過節點操作對比來實現。但是我們對編譯template字符串 => 代理data數據/methods的this綁定 => 數據觀察 => 建立watch及更新渲染的大致流程有了個比較完整的認知。

歡迎到前端學習打卡群一起學習~516913974

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

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

擁有後台管理系統的網站,將擁有強大的資料管理與更新功能,幫助您隨時新增網站的內容並節省網站開發的成本。

02 . Tomcat集群會話共享

redis簡介

redis是一個key-value存儲系統。和Memcached類似,它支持存儲的value類型相對更多,包括string(字符串)、list(鏈表)、set(集合)、zset(sorted set –有序集合)和hash(哈希類型)。與memcached一樣,為了保證效率,數據都是緩存在內存中。區別的是redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件,並且在此基礎上實現master-slave(主從)同步。

Redis詳細請看我專門寫的redis

https://www.cnblogs.com/you-men/tag/Redis/

如何保持session會話

目前,為了使web能適應大規模的訪問,需要實現應用的集群部署。集群最有效的方案就是負載均衡,而實現負載均衡用戶每一個請求都有可能被分配到不固定的服務器上,這樣我們首先要解決session的統一來保證無論用戶的請求被轉發到哪個服務器上都能保證用戶的正常使用,即需要實現session的共享機制。

在集群系統下實現session統一的有如下幾種方案:

1、請求精確定位:sessionsticky,例如基於訪問ip的hash策略,即當前用戶的請求都集中定位到一台服務器中,這樣單台服務器保存了用戶的session登錄信息,如果宕機,則等同於單點部署,會丟失,會話不複製。

2、session複製共享:sessionreplication,如tomcat自帶session共享,主要是指集群環境下,多台應用服務器之間同步session,使session保持一致,對外透明。 如果其中一台服務器發生故障,根據負載均衡的原理,調度器會遍歷尋找可用節點,分發請求,由於session已同步,故能保證用戶的session信息不會丟失,會話複製,。

此方案的不足之處:

必須在同一種中間件之間完成(如:tomcat-tomcat之間).

session複製帶來的性能損失會快速增加.特別是當session中保存了較大的對象,而且對象變化較快時, 性能下降更加顯著,會消耗系統性能。這種特性使得web應用的水平擴展受到了限制。

Session內容通過廣播同步給成員,會造成網絡流量瓶頸,即便是內網瓶頸。在大併發下錶現並不好

3、基於cache DB緩存的session共享

基於memcache/redis緩存的 session 共享

即使用cacheDB存取session信息,應用服務器接受新請求將session信息保存在cache DB中,當應用服務器發生故障時,調度器會遍歷尋找可用節點,分發請求,當應用服務器發現session不在本機內存時,則去cache DB中查找,如果找到則複製到本機,這樣實現session共享和高可用。

nginx+tomcat+redis實現負載均衡、session共享

環境
主機 操作系統 IP地址 硬件/網絡
Nginx CentOS7.3 39.108.140.0 1C2G / 公有雲
Tomcat-1 CentOS7.3 121.36.43.2 1C2G / 公有雲
Tomcat-2 CentOS7.3 49.233.69.195 1C2G / 公有雲
Redis CentOS7.3 116.196.83.113 1C2G / 公有雲
MySQL CentOS7.3 116.196.83.113 1C2G / 公有雲
實驗拓撲

在這個圖中,nginx做為反向代理,實現靜動分離,將客戶動態請求根據權重隨機分配給兩台tomcat服務器,redis做為兩台tomcat的共享session數據服務器,mysql做為兩台tomcat的後端數據庫。

nginx安裝配置

使用Nginx作為Tomcat的負載平衡器,Tomcat的會話Session數據存儲在Redis,能夠實現零宕機的7×24效果。因為將會話存儲在Redis中,因此Nginx就不必配置成stick粘貼某個Tomcat方式,這樣才能真正實現後台多個Tomcat負載平衡。

部署nginx

#!/usr/bin/env bash
# Author: ZhouJian
# Mail: 18621048481@163.com
# Time: 2019-9-3
# Describe: CentOS 7 Install Nginx Source Code Script

version="nginx-1.14.2.tar.gz"
user="nginx"
nginx=${version%.tar*}
path=/usr/local/src/$nginx
echo $path
if ! ping -c2 www.baidu.com &>/dev/null
then
	echo "網絡不通,無法安裝"
	exit
fi

yum install -y gcc gcc-c++ openssl-devel pcre-devel make zlib-devel wget psmisc
if [ ! -e $version ];then
	wget http://nginx.org/download/$version
fi
if ! id $user &>/dev/null
then
	useradd $user -M -s /sbin/nologin
fi

if [ ! -d /var/tmp/nginx ];then
	mkdir -p /var/tmp/nginx/{client,proxy,fastcgi,uwsgi,scgi}
fi
tar xf $version -C /usr/local/src
cd $path
./configure \
--prefix=/usr/local/nginx \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_flv_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_gzip_static_module \
--with-http_auth_request_module \
--with-http_random_index_module \
--with-http_realip_module \
--http-client-body-temp-path=/var/tmp/nginx/client \
--http-proxy-temp-path=/var/tmp/nginx/proxy \
--http-fastcgi-temp-path=/var/tmp/nginx/fastcgi \
--http-uwsgi-temp-path=/var/tmp/nginx/uwsgi \
--http-scgi-temp-path=/var/tmp/nginx/scgi \
--with-pcre \
--with-file-aio \
--with-http_secure_link_module && make && make install
if [ $? -ne 0 ];then
	echo "nginx未安裝成功"
	exit
fi

killall nginx
/usr/local/nginx/sbin/nginx
#echo "/usr/local/nginx/sbin/nginx" >> /etc/rc.local
#chmod +x /etc/rc.local
#systemctl start rc-local
#systemctl enable rc-local
ss -antp |grep nginx

配置nginx反向代理:反向代理+負載均衡+健康探測,nginx.conf文件內容:

vim /usr/local/nginx/conf/nginx.conf
worker_processes  4;
events {
        worker_connections  1024;
}
    http {
        include       mime.types;
        default_type  application/octet-stream;
        sendfile        on;
        keepalive_timeout  65;
        log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                        '$status $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for"';

    #blog lb by oldboy at 201303
        upstream backend_tomcat {
        #ip_hash;
        server 192.168.6.241:8080   weight=1 max_fails=2 fail_timeout=10s;
        server 192.168.6.242:8080   weight=1 max_fails=2 fail_timeout=10s;
        #server 192.168.6.243:8080   weight=1 max_fails=2 fail_timeout=10s;
        }

        server {
            listen       80;
            server_name  www.98yz.cn;
            charset utf-8;
            location / {
                root html;
                index  index.jsp index.html index.htm;
                    }
            location ~* \.(jsp|do)$ {
            proxy_pass  http://backend_tomcat;
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
                }
        }

    }
安裝部署tomcat應用程序服務器

在tomcat-1和tomcat-2節點上安裝JDK

在安裝tomcat之前必須先安裝JDK,JDK的全稱是java development kit,是sun公司免費提供的java語言的軟件開發工具包,其中包含java虛擬機(JVM),編寫好的java源程序經過編譯可形成java字節碼,只要安裝了JDK,就可以利用JVM解釋這些字節碼文件,從而保證了java的跨平台性。

安裝JDK,Tomcat 程序

tar xvf jdk-8u151-linux-x64.tar.gz -C /usr/local/
wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.55/bin/apache-tomcat-8.5.55.tar.gz
tar xf apache-tomcat-8.5.55.tar.gz -C /usr/local/
cd /usr/local/
mv apache-tomcat-8.5.55/ tomcat
mv jdk1.8.0_151/ jdk

按照相同方法在tomcat-2也安裝

vim conf/server.xml

// 設置默認虛擬主機,並增加jvmRoute
<Engine name="Catalina" defaultHost="localhost" jvmRoute="tomcat-1">
  
// 修改默認虛擬主機,並將網站文件路徑指向/web/webapp1,在host段增加context段  
<Host name="localhost"  appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Context docBase="/web/webapp1" path="" reloadable="true"/>
</Host>
  
  
// 增加文檔目錄與測試文件  
mkdir -p /web/webapp1
cd /web/webapp1
cat index.jsp 
<%@page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<html>
    <head>
        <title>tomcat-1</title>
    </head>
    <body>
        <h1><font color="red">Session serviced by tomcat</font></h1>
        <table aligh="center" border="1">
        <tr>
            <td>Session ID</td>
            <td><%=session.getId() %></td>
                <% session.setAttribute("abc","abc");%>
            </tr>
            <tr>
            <td>Created on</td>
            <td><%= session.getCreationTime() %></td>
            </tr>
        </table>
    tomcat-1
    </body>
<html>  
  
  
// 接下來我們將tomcat和nginx都啟動起來,可以發現用戶訪問index.jsp會一會跳轉tomcat1,一會tomcat2,session還不一致  

Tomcat-2節點與tomcat-1節點配置基本類似,只是jvmRoute不同,另外為了區分由哪個節點提供訪問,測試頁標題也不同(生產環境兩個tomcat服務器提供的網頁內容是相同的)。其他的配置都相同。

用瀏覽器訪問nginx主機,驗證負載均衡

驗證健康檢查的方法可以關掉一台tomcat主機,用客戶端瀏覽器測試訪問。

從上面的結果能看出兩次訪問,nginx把訪問請求分別分發給了後端的tomcat-1和tomcat-2,客戶端的訪問請求實現了負載均衡,但sessionid並一樣。所以,到這裏我們準備工作就全部完成了,下面我們來配置tomcat通過redis實現會話保持。

安裝redis
yum -y install gcc
wget http://download.redis.io/releases/redis-4.0.14.tar.gz
tar xvf redis-4.0.14.tar.gz -C /opt/
cd /opt/redis-4.0.14

編譯安裝

# Redis的編譯,只將命令文件編譯,將會在當前目錄生成bin目錄
make && make install  PREFIX=/usr/local/redis
cd ..
mv redis-4.0.14/* /usr/local/redis/

# 創建環境變量
echo 'PATH=$PATH:/usr/local/redis/src/' >> /etc/profile
source /etc/profile

# 此時在任何目錄位置都可以是用redis-server等相關命令
[root@redis1 ~]# redis-
redis-benchmark  redis-check-rdb  redis-sentinel   redis-trib.rb    
redis-check-aof  redis-cli        redis-server 

配置Redis

# 設置後台啟動
# 由於Redis默認是前台啟動,不建議使用.可以修改為後台
daemonize yes


# 禁止protected-mode yes/no(保護模式,是否只允許本地訪問)
protected-mode


# 設置遠程訪問
# Redis默認只允許本機訪問,把bind修改為bind 0.0.0.0 此設置會變成允許所有遠程訪問,如果指定限制訪問,可設置對應IP。
# bind指定是redis所在服務器網卡的IP,不指定本機網卡IP,可能導致你的Redis實例無法啟動
# 如果想限制IP訪問,內網的話通過網絡接口(網卡限定),讓客戶端訪問固定網卡鏈接redis
# 如果是公網,通過iptables指定某個IP允許訪問
bind 0.0.0.0

# 配置Redis日誌記錄
# 找到logfile,默認為logfile "",改為自定義日誌格式
logfile  /var/log/redis_6379.log

# 把requirepass修改為123456,修改之後重啟下服務
requirepass "123456"
# 不重啟Redis設置密碼
# 在配置文件中配置requirepass的密碼(當Redis重啟時密碼依然生效)
127.0.0.1:6379> config set requirepass test123
# 查詢密碼
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "test123"

# 密碼驗證
127.0.0.1:6379> auth test123
OK
127.0.0.1:6379> set name flying
OK
127.0.0.1:6379> get name
"flying"

# 遠程主機連接
# redis-cli  -h  redis_ip -p redis_port -a password

啟動測試

# 放到後台輸出,redis自帶日誌了,可以輸出到黑洞
nohup redis-server /usr/local/redis/redis.conf &> /usr/local/redis/redis.log &

# 關閉命令
redis-cli -h 127.0.0.1 -p 6379 -a 123456 shutdown
# 注意:不建議使用 kill -9,這種方式不但不會做持久化操作,還會造成緩衝區等資源不能優雅關閉。極端情況下造成 AOF 和 複製丟失數據 的情況。
# shutdown 還有一個參數,代表是否在關閉 redis 前,生成 持久化文件,命令為 redis-cli shutdown nosave|save。


# 設置開機自啟動
echo "redis-server /usr/local/redis.conf" >> /etc/rc.local

配置tomcat session redis同步

通過TomcatClusterRedisSessionManager,這種方式支持redis3.0的集群方式
下載TomcatRedisSessionManager-2.0.zip包,https://github.com/ran-jit/tomcat-cluster-redis-session-manager,放到$TOMCAT_HOMA/lib下,並解壓

cd /usr/local/tomcat/lib/
wget https://github.com/ran-jit/tomcat-cluster-redis-session-manager/releases/download/2.0.4/tomcat-cluster-redis-session-manager.zip
unzip tomcat-cluster-redis-session-manager.zip 
cp tomcat-cluster-redis-session-manager/lib/* ./
cp tomcat-cluster-redis-session-manager/conf/redis-data-cache.properties ../conf/
cat ../conf/redis-data-cache.properties     
#-- Redis data-cache configuration
//遠端redis數據庫的地址和端口
#- redis hosts ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, ....
redis.hosts=192.168.6.244:6379
//遠端redis數據庫的連接密碼
#- redis password (for stand-alone mode)
redis.password=pwd@123
//是否支持集群,默認的是關閉
#- set true to enable redis cluster mode
redis.cluster.enabled=false
//連接redis的那個庫
#- redis database (default 0)
#redis.database=0
//連接超時時間
#- redis connection timeout (default 2000)
#redis.timeout=2000
//在這個<Context>標籤裏面配置

vim ../conf/context.xml
<Valve className="tomcat.request.session.redis.SessionHandlerValve" />
<Manager className="tomcat.request.session.redis.SessionManager" />

配置會話到期時間在../conf/web.xml

<session-config>
<session-timeout>60</session-timeout>
</session-config>

啟動tomcat服務

[root@linux-node2 lib]# ../bin/startup.sh

Tomcat-2節點與tomcat-1節點配置相同

測試,我們每次強刷他的sessionID都是一致的,所以我們認為他的session會話保持已經完成,你們也可以選擇換個客戶端的IP地址來測試

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準

信道估計(channel estimation)圖解——從SISO到MIMO原理介紹

1. 引言

在所有通信中,信號都會通過一個介質(稱為信道),並且信號會失真,或者在信號通過信道時會向信號中添加各種噪聲。正確解碼接收到的信號而沒有太多錯誤的方法是從接收到的信號中消除信道施加的失真和噪聲。為此,第一步是弄清信號經過的信道的特性。表徵信道的技術/過程稱為信道估計(channel estimation)。此過程將說明如下。

信道估計有很多不同的方法,但是基本概念是相似的。該過程如下進行。

i)設置一個數學模型,以使用“信道”矩陣將“發射信號”和“接收信號”相關。

ii)發射已知信號(我們通常將其稱為“參考信號”或“導頻信號”)並檢測接收到的信號。

iii)通過比較發送信號和接收信號,我們可以找出信道矩陣的每個元素。

作為此過程的示例,這裏簡要介紹LTE中的此過程。當然,很多細節取決於實現(這意味着具體算法可能會因每個特定的芯片組實現而有所不同)。但是,總體概念將是相似的。

2. 通用算法

我們如何找出信道的屬性?即,我們如何估計信道?從高的角度來看,可以如下圖所示。此圖显示以下內容:

i)我們嵌入了一組預定義信號(這稱為參考信號)

ii)當這些參考信號通過信道時,它會與其他信號一起失真(衰減,相移,噪聲)

iii)我們在接收方檢測/解碼接收到的參考信號

iv)比較發送的參考信號和接收的參考信號,並找到它們之間的相關性。

3. SISO的信道估計

現在讓我們考慮LTE SISO的情況,看看如何估計信道屬性(信道係數和噪聲估計)。由於考慮的是SISO系統,因此參考信號僅嵌入到一個天線端口(端口0)中。資源圖中的垂直線表示頻域。因此,這裏用f1,f2,f3 … fn索引了每個參考信號。每個參考符號可以是一個複數(I / Q數據),可以如下所示進行繪製。左側(發送側)的每個複數(參考符號)被修改(失真)為右側的每個對應符號(接收的符號)。信道估計是在左側的複數數組與右側的複數數組之間找到相關性的過程。

估計的詳細方法可能非常取決於實現方式。這裏將描述的方法基於開源:srsLTE(請參閱[1])

3.1 信道係數的估計

由於這裏只有一根天線,因此每個發射參考信號和接收參考信號的系統模型可以表示如下。y()表示接收到的參考信號的數組,x()表示發送的參考信號()的數組,h()表示信道係數的數組。f1,f2,…只是整數索引。

我們知道x()是什麼,因為給定了它,而y()也知道,因為它是從接收者處測量/檢測到的。有了這些,我們可以很容易地計算出係數陣列,如下所示。

現在我們有了參考信號所在位置的所有信道係數。但是我們需要在所有位置(包括那些沒有參考信號的點)處的信道效率。這意味着我們需要在沒有參考信號的情況下找出那些位置的信道係數。為此,最常見的方法是對測得的係數數組進行插值。在srsLTE的情況下,它首先進行平均,然後對平均信道係數進行插值。

3.2 噪聲的估計

下一步是估計噪聲特性。從理論上講,噪聲可以如下計算。

但是,我們需要的是噪聲的統計屬性,而不是確切的噪聲值。我們可以僅使用測得的信道係數和平均信道來估算噪聲,如下所示(實際上,準確的噪聲值沒有太大意義,因為噪聲值會不斷變化,使用那些特定的噪聲值沒有用)。在srsLTE中,作者使用了這種方法。

4. 2 x 2 MIMO的信道估計

假設我們有一個如下所示的通信系統。x(t)表示發送信號,y(t)表示接收信號。當x(t)傳輸到空中(信道)時,它會變形並獲得各種噪聲,並且可能會相互干擾。因此接收到的信號y(t)不能與發射信號x(t)相同。

發射信號,接收信號和信道矩陣之間的關係可以用數學形式建模,如下所示。

在此等式中,我們知道值x1,x2(已知的發射信號)和y1,y2(檢測/接收的信號)。我們不知道的部分是H矩陣和噪聲(n1,n2)。

為簡單起見,我們假設該信道中沒有噪聲,這意味着我們可以將n1,n2設置為0。(當然,在實際信道中總會存在噪聲,估計噪聲是信道估計中非常重要的一部分,但是我們在此示例中假設沒有噪音,只是為了使其簡單。稍後,當我有更好的知識以通俗的語言描述案件時,我將在案件中添加噪音)。

由於我們具有數學模型,因此下一步是傳輸已知信號(參考信號)並從參考信號中找出信道參數。

假設我們僅通過一個天線發送了幅度為1的已知信號,而另一個天線現在處於關閉狀態。由於信號通過空氣傳播,並且接收方的兩個天線都會檢測到該信號。現在,假設第一個天線接收幅度為0.8的參考信號,第二個天線接收幅度為0.2的參考信號。有了這個結果,我們可以得出如下所示的一行信道矩陣(H)。

假設我們僅通過另一個(第二個)天線發送了幅度為1的已知信號,並且第一個天線現在處於關閉狀態。由於信號通過空氣傳播,並且接收方的兩個天線都會檢測到該信號。現在,假設第一個天線接收到幅度為0.3的參考信號,第二個天線接收到幅度為0.7的參考信號。有了這個結果,我們可以得出如下所示的一行信道矩陣(H)。

夠簡單嗎?我認為理解這個基本概念沒有任何問題。但是,如果完全按照上述方法使用此方法,則可能會導致效率低下。根據上面解釋的概念,應該有一個時刻,僅發送參考信號而沒有實際數據,只是為了估計信道信息,這意味着由於信道估計過程,數據速率將降低。為了消除這種效率低下的問題,實際的通信系統會同時發送參考信號和數據。

現在的問題是“如何在同時傳輸參考信號和數據的同時實現上述概念?”。可以有幾種不同的方法來執行此操作,並且不同的通信系統將使用一些不同的方法。

以LTE為例,我們使用如下所示的方法。在LTE中為2 x 2 MIMO的情況下,每個子幀具有用於每個天線的參考信號的不同位置。天線0的子幀發送了分配給天線0的參考信號,不發送分配給天線1的參考信號的信號。天線1的子幀發送了分配給天線1的參考信號的信號,不發送給參考天線的任何信號。為天線0分配的信號。因此,如果在兩個接收器天線上解碼為天線0的參考信號分配的資源元素,則可以估計h11,h12。(在這裏,為了簡單起見,我們還假設沒有噪音)。如果在兩個接收器天線上解碼分配給天線1參考信號的資源元素,則可以估計h21,h22。

4.1 信道係數的估計

上面說明的過程是針對LTE OFDMA符號中的頻域中的一個特定點測量 \(H\) 矩陣。如果您在對符號的其他部分進行解碼的過程中照原樣應用測量的H值,則解碼的符號的準確性可能不盡人意,因為上一步中使用的測量數據會包含一定程度的噪聲。因此,在實際應用中,對通過上述方法測得的 \(H\) 值進行某種后處理,在此後處理過程中,我們可以找出噪聲的總體統計屬性(例如,噪聲的均值,方差和統計分佈))。要記住的一件事是,在此過程中獲得的特定噪聲值本身並沒有太多意義。從參考信號獲得的特定值將與用於解碼其他數據的噪聲值(非參考信號)不同,因為噪聲值是隨機變化的。然而,那些隨機噪聲的總體特性可以是重要的信息(例如,在SNR估計等中使用)。

在繼續之前,讓我們再次簡單地考慮一下數學模型。即使我們將系統方程式描述如下,其中包括噪聲項,但這並不意味着您可以直接測量噪聲。是不可能的。該方程式僅表明檢測到的信號(y)包含噪聲分量的某些部分。

因此,當我們測量信道係數時,我們使用了沒有噪聲項的設備,如下所示。

在LTE的特定應用中,我們在OFDM符號中有多個測量點(多個參考信號)。這些測量點在頻域上表示。因此,讓我們如下重寫信道矩陣以指示每個信道矩陣的測量點。

現在,假設您已經測量了整個OFDM符號上的H矩陣,那麼您將擁有多個 \(H\) 矩陣,如下所示,每個矩陣都以一個特定的頻率指示H矩陣。

現在你有了一個 \(H\) 矩陣數組。該陣列由四個不同的組組成,每個組用不同的顏色突出显示,如下所示。

當應用后處理算法時,該算法需要分別應用於這些組中的每一個。因此,為簡單起見,我將 \(H\) 矩陣的數組重新排列為多個獨立數組(在本例中為4個數組),如下所示。

對於這些數組中的每一個,我將進行如下所示的相同處理。(每個芯片組製造商都可以應用稍微不同的方法,但是總體思路是相似的)。在下面說明的方法中,數據(每個頻點中的信道係數陣列)使用IFFT進行處理,這意味着將dta轉換為時域,從而生成標記為(2)的時域數據陣列。實際上,這是特定信道路徑的脈衝響應。然後,我們對該時域數據應用特定的過濾(或加窗)。在此示例中,將某個點的數據替換為零,並創建標記為(3)的結果。您可以應用更複雜的過濾器或窗口,而不是這種簡單的調零。然後,通過將濾波后的信道脈衝數據轉換回頻域,

通過對所有四個陣列執行相同的過程,您可以獲得“估計信道係數陣列”的四個陣列。從這四個陣列中,您可以按以下方式重建估計信道矩陣的陣列。

4.2 噪聲的估計

使用此估算的信道矩陣,您可以使用以下公式估算每個點的噪聲值。這與本頁開頭的原始系統方程式相同,除了將H矩陣替換為“估計的H”矩陣外,現在我們知道除噪聲值以外的所有值。因此,通過插入所有已知值,我們可以在每個測量點計算(估計)噪聲值。

如果將此方程式應用於所有測量點,則將獲得所有測量點的噪聲值,並從這些計算出的噪聲值中獲得噪聲的統計屬性。如上所述,此處計算出的每個單獨的噪聲值沒有太大意義,因為該值不能直接應用於解碼其他信號(非參考信號),但是這些噪聲的統計特性對於確定噪聲而言可能是非常有用的信息。渠道的性質。

注意:如果您對在實際應用中如何使用此算法感興趣,強烈建議閱讀/嘗試使用Ref [2]和[3]。

參考:

[1] srsLTE:\ srslte \ lib \ ch_estimation \ chest_dl.c-srslte_chest_dl_estimate_port()

[2] 信道估計(Mathworks,LTE工具箱)

[3] NR同步程序

[4] http://www.sharetechnote.com/html/Communication_ChannelEstimation.html#General_Algorithm

更多精彩內容請關注訂閱號優化與算法和加入QQ討論群1032493483獲取更多資料

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

「從零單排canal 01」 canal 10分鐘入門(基於1.1.4版本)

1.簡介

canal [kə’næl],譯意為水道/管道/溝渠,主要用途是基於 MySQL 數據庫增量日誌解析,提供增量數據 訂閱 和 消費。應該是阿里雲DTS(Data Transfer Service)的開源版本。

2.提供的能力

Canal與DTS提供的功能基本相似:

1)基於Mysql的Slave協議實時dump binlog流,解析為事件發送給訂閱方。

2)單Canal instance,單DTS數據訂閱通道均只支持訂閱一個RDS,提供給一個消費者。

3)可以使用canal-client客戶端進行消息消費。

4)也可以通過簡單配置,也可以不需要自行使用canal-client消費,可以選擇直接投遞到kafka或者RocketMQ集群,用戶只需要使用消息隊列的consumer消費即可。

5)成功消費消息后需要進行Ack,以確保一致性,服務端則會維護客戶端目前的消費位點。

3.工作原理

MySQL的主從複製分成三步:

  • master將改變記錄到二進制日誌(binary log)中(這些記錄叫做二進制日誌事件,binary log events,可以通過show binlog events進行查看);
  • slave將master的binary log events拷貝到它的中繼日誌(relay log);
  • slave重做中繼日誌中的事件,將改變反映它自己的數據。

 

canal 就是模擬了這個過程。

  • canal模擬 MySQL slave 的交互協議,偽裝自己為 MySQL slave ,向 MySQL master 發送 dump 協議;
  • MySQL master 收到 dump 請求,開始推送 binary log 給 slave (即 canal );
  • canal 解析 binary log 對象(原始為 byte 流);

 

4. canal 架構

4.1 admin版本整體架構

canal 1.1.4開始支持admin管理,通過canal-admin為canal提供整體配置管理、節點運維等面向運維的功能,提供相對友好的WebUI操作界面,方便更多用戶快速和安全的操作,替代了過去繁瑣的配置文件管理。

整體部署架構如下。

 

  • 多個canal-server可以組成集群模式,每個instance任務通過zookeeper在集群中實現高可用
  • 通過多個集群,可以實現同步資源的物理隔離
  • 可以直接抓取消費投遞MQ,可以實現生產/消費解耦、消息堆積、消息回溯
  • 可以抓取消費投遞給canal-client,在用戶的服務中進行消息處理,減少中間過程

4.2 canal-server架構

 

說明:

  • server代表一個canal-server運行實例,對應於一個jvm
  • instance對應於一個數據隊列,是真正的變更抓取的實體 (1個server可以對應多個instance)

Instance模塊

  • EventParser :數據源接入,模擬slave協議和master進行交互,協議解析
  • EventSink :Parser和Store鏈接器,進行數據過濾,加工,分發的工作
  • EventStore :數據存儲
  • MetaManager:增量訂閱&消費信息管理器

1)EventParser子模塊

EventParser模塊的類圖設計如下

 

每個EventParser都會關聯兩個內部組件:CanalLogPositionManager , CanalHAController

  • CanalLogPositionManager:記錄binlog最後一次解析成功位置信息,主要是描述下一次canal啟動的位點
  • CanalHAController:支持Mysql主備,判斷當前該連哪個mysql(基於Heartbeat實現,主庫失去心跳則連備庫)

EventParser根據HAController獲知連到哪裡,通過LogPositionManager獲知從哪個位點開始解析,之後便通過Mysql Slave協議拉取binlog進行解析,推入EventSink

2)EventSink子模塊

目前只提供了一個帶有實際作用的實現:GroupEventSink

GroupEventSink用於將多個instance上的數據進行歸併,常用於分庫后的多數據源歸併。

3)EventStore子模塊

EventStore的類圖如下

 

官方提供的實現類是
MemoryEventStoreWIthBuffer,內部採用的是一個RingBuffer:

 

  • Put : Sink模塊進行數據存儲的最後一次寫入位置
  • Get : 數據訂閱獲取的最後一次提取位置
  • Ack : 數據消費成功的最後一次消費位置

這些位點信息通過MetaManager進行管理。這也解釋了為什麼一個canal instance只能支撐一個消費者:EventStore的RingBuffer只為一個消費者維護信息。

4.3 客戶端使用

數據格式已經在前文給出,Canal和DTS客戶端均採取:

拉取事件 -> 消費 -> 消費成功后ACK

這樣的消費模式,並支持消費不成功時進行rollback,重新消費該數據。

下面是一段簡單的客戶端調用實例(略去異常處理):

// 創建CanalConnector, 連接到localhost:11111

CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress(AddressUtils.getHostIp(),11111), destination, "", "");

connector.connect(); // 連接

connector.subscribe(); // 開始訂閱binlog

// 開始循環拉取

while (running) {

Message message = connector.getWithoutAck(1024); // 獲取指定數量的數據

long batchId = message.getId();

for (Entry entry : message.getEntries()){

// 對每條消息進行處理

}

connector.ack(batchId); // ack

}

5.總結分析

5.1 優點

1)性能優異、功能全面

  • canal 1.1.x 版本(release_note),性能與功能層面有較大的突破,重要提升包括:
  • 整體性能測試&優化,提升了150%. #726
  • 原生支持prometheus監控 #765
  • 原生支持kafka消息投遞 #695
  • 原生支持aliyun rds的binlog訂閱 (解決自動主備切換/oss binlog離線解析) (無法拒絕它的理由!)
  • 原生支持docker鏡像 #801

2)運維方便

  • canal 1.1.4版本,迎來最重要的WebUI能力,引入canal-admin工程,支持面向WebUI的canal動態管理能力,支持配置、任務、日誌等在線白屏運維能力
  • Standalone的一體化解決方案,無外部服務依賴,運維更簡單,在某種程度上也意味着更穩定。
  • 開箱即用,節約開發與定製成本。
  • 有良好的管理控制平台與監控系統(如果你已經有promethus監控,可以秒接canal監控)

3)多語言支持

  • canal 特別設計了 client-server 模式,交互協議使用 protobuf 3.0 , client 端可採用不同語言實現不同的消費邏輯
  • canal 作為 MySQL binlog 增量獲取和解析工具,可將變更記錄投遞到 MQ 系統中,比如 Kafka/RocketMQ,可以藉助於 MQ 的多語言能力

5.2 缺點

  • 單instance/訂閱通道只支持訂閱單個數據庫,並只能支持單客戶端消費。每當我們需要新增一個消費端->MySQL的訂閱:對於Canal而言,就要給MySQL接一個“Slave”,可能會對主庫有一定影響。
  • 消息的Schema很弱,所有消息的Schema均相同,客戶端需要提前知道各個表消息的Schema與各字段的上下文才能正確消費。

好了,花了10分鐘應該對canal有大致了解了,下一期,阿丸計劃手把手教你搭建canal集群和admin管理平台,記得關注哦。

 

都看到最後了,原創不易,點個關注,點個贊吧~

知識碎片重新梳理,構建Java知識圖譜: github.com/saigu/JavaK…(歷史文章查閱非常方便)

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

※推薦台中搬家公司優質服務,可到府估價

架構設計 | 異步處理流程,多種實現模式詳解

本文源碼:GitHub·點這裏 || GitEE·點這裏

一、異步處理

1、異步概念

異步處理不用阻塞當前線程來等待處理完成,而是允許後續操作,直至其它線程將處理完成,並回調通知此線程。

必須強調一個基礎邏輯,異步是一種設計理念,異步操作不等於多線程,MQ中間件,或者消息廣播,這些是可以實現異步處理的方式。

同步處理和異步處理相對,需要實時處理並響應,一旦超過時間會結束會話,在該過程中調用方一直在等待響應方處理完成並返回。同步類似電話溝通,需要實時對話,異步則類似短信交流,發送消息之後無需保持等待狀態。

2、異步處理優點

雖然異步處理不能實時響應,但是處理複雜業務場景,多數情況都會使用異步處理。

  • 異步可以解耦業務間的流程關聯,降低耦合度;
  • 降低接口響應時間,例如用戶註冊,異步生成相關信息表;
  • 異步可以提高系統性能,提升吞吐量;
  • 流量削峰即把請求先承接下來,然後在異步處理;
  • 異步用在不同服務間,可以隔離服務,避免雪崩;

異步處理的實現方式有很多種,常見多線程,消息中間件,發布訂閱的廣播模式,其根據邏輯在於先把請求承接下來,放入容器中,在從容器中把請求取出,統一調度處理。

注意:一定要監控任務是否產生積壓過度情況,任務如果積壓到雪崩之勢的地步,你會感覺每一片雪花都想勇闖天涯。

3、異步處理模式

異步流程處理的實現有好多方式,但是實際開發中常用的就那麼幾種,例如:

  • 基於接口異步響應,常用在第三方對接流程;
  • 基於消息生產和消費模式,解耦複雜流程;
  • 基於發布和訂閱的廣播模式,常見系統通知

異步適用的業務場景,對數據強一致性的要求不高,異步處理的數據更多時候追求的是最終一致性。

二、接口響應異步

1、流程描述

基於接口異步響應的方式,有一個本地業務服務,第三方接口服務,流程如下:

  • 本地服務發起請求,調用第三方服務接口;
  • 請求包含業務參數,和成功或失敗的回調地址;
  • 第三方服務實時響應流水號,作為該調用的標識;
  • 之後第三方服務處理請求,得到最終處理結果;
  • 如果處理成功,回調本地服務的成功通知接口;
  • 如果處理失敗,回調本地服務的失敗通知接口;
  • 整個流程基於部分異步和部分實時的模式,完整處理;

注意:如果本地服務多次請求第三方服務,需要根據流水號判斷該請求的狀態,業務的狀態設計也是極其複雜,要根據流水號和狀態追溯整個流程的執行進度,避免錯亂。

2、流程實現案例

模擬基礎接口

@RestController
public class ReqAsyncWeb {
    private static final Logger LOGGER = LoggerFactory.getLogger(ReqAsyncWeb.class);
    @Resource
    private ReqAsyncService reqAsyncService ;
    // 本地交易接口
    @GetMapping("/tradeBegin")
    public String tradeBegin (){
        String sign = reqAsyncService.tradeBegin("TradeClient");
        return sign ;
    }
    // 交易成功通知接口
    @GetMapping("/tradeSucNotify")
    public String tradeSucNotify (@RequestParam("param") String param){
        LOGGER.info("tradeSucNotify param={"+ param +"}");
        return "success" ;
    }
    // 交易失敗通知接口
    @GetMapping("/tradeFailNotify")
    public String tradeFailNotify (@RequestParam("param") String param){
        LOGGER.info("tradeFailNotify param={"+ param +"}");
        return "success" ;
    }
    // 第三方交易接口
    @GetMapping("/respTrade")
    public String respTrade (@RequestParam("param") String param){
        LOGGER.info("respTrade param={"+ param +"}");
        reqAsyncService.respTrade(param);
        return "NO20200520" ;
    }
}

模擬第三方處理

@Service
public class ReqAsyncServiceImpl implements ReqAsyncService {

    private static final String serverUrl = "http://localhost:8005" ;

    @Override
    public String tradeBegin(String param) {
        String orderNo = HttpUtil.get(serverUrl+"/respTrade?param="+param);
        if (StringUtils.isEmpty(orderNo)){
            return "Trade..Fail...";
        }
        return orderNo ;
    }

    @Override
    public void respTrade(String param) {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Thread thread01 = new Thread(
                new RespTask(serverUrl+"/tradeSucNotify?param="+param),"SucNotify");
        Thread thread02 = new Thread(
                new RespTask(serverUrl+"/tradeFailNotify?param="+param),"FailNotify");
        thread01.start();
        thread02.start();
    }
}

三、生產消費異步

1、流程描述

這裏基於Kafka中間件,演示流程消息生成,消息處理的異步解耦流程,基本步驟:

  • 消息生成之後,寫入Kafka隊列 ;
  • 消息處理方獲取消息后,進行流程處理;
  • 消息在中間件提供的隊列中持久化存儲 ;
  • 消息發起方如果掛掉,不影響消息處理 ;
  • 消費方如果掛掉,不影響消息生成;

基於這種消息中間件模式,完成業務解耦,提高系統吞吐量,是架構中常用的方式。

2、流程實現案例

消息發送

@Service
public class KafkaAsyncServiceImpl implements KafkaAsyncService {

    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    @Override
    public void sendMsg(String msg) {
        // 這裏Topic如果不存在,會自動創建
        kafkaTemplate.send("kafka-topic", msg);
    }
}

消息消費

@Component
public class KafkaConsumer {

    private static Logger LOGGER = LoggerFactory.getLogger(KafkaConsumer.class);

    @KafkaListener(topics = "kafka-topic")
    public void listenMsg (ConsumerRecord<?,String> record) {
        String value = record.value();
        LOGGER.info("KafkaConsumer01 ==>>"+value);
    }
}

注意:這裏就算有多個消息消費方,也只會在一個消費方處理消息,這就是該模式的特點。

四、發布訂閱異步

1、流程描述

這裏基於Redis中間件,說明消息廣播模式流程,基本步驟:

  • 提供一個消息傳遞頻道channel;
  • 多個訂閱頻道的客戶端client;
  • 消息通過PUBLISH命令發送給頻道channel ;
  • 客戶端就會收到頻道中傳遞的消息 ;

之所以稱為廣播模式,該模式更注重通知下發,流程交互性不強。實際開發場景:運維總控系統,更新了某類服務配置,通知消息發送之後,相關業務線上的服務在拉取最新配置,更新到服務中。

2、流程實現案例

發送通知消息

@Service
public class RedisAsyncServiceImpl implements RedisAsyncService {

    @Resource
    private StringRedisTemplate stringRedisTemplate ;

    @Override
    public void sendMsg(String topic, String msg) {
        stringRedisTemplate.convertAndSend(topic,msg);
    }
}

客戶端接收

@Service
public class ReceiverServiceImpl implements ReceiverService {

    private static final Logger LOGGER = LoggerFactory.getLogger("ReceiverMsg");

    @Override
    public void receiverMsg(String msg) {
        LOGGER.info("Receiver01 收到消息:msg-{}",msg);
    }
}

配置廣播模式

@Configuration
public class SubMsgConfig {

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory factory,
                                            MessageListenerAdapter msgListenerAdapter,
                                            MessageListenerAdapter msgListenerAdapter02){
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        //註冊多個監聽,訂閱一個主題,實現消息廣播
        container.addMessageListener(msgListenerAdapter, new PatternTopic("topic:msg"));
        container.addMessageListener(msgListenerAdapter02, new PatternTopic("topic:msg"));
        return container;
    }

    @Bean
    MessageListenerAdapter msgListenerAdapter(ReceiverService receiverService){
        return new MessageListenerAdapter(receiverService, "receiverMsg");
    }
    @Bean
    MessageListenerAdapter msgListenerAdapter02(ReceiverService02 receiverService02){
        return new MessageListenerAdapter(receiverService02, "receiverMsg");
    }

    @Bean
    ReceiverService receiverService(){
        return new ReceiverServiceImpl();
    }
    @Bean
    ReceiverService02 receiverService02(){
        return new ReceiverServiceImpl02();
    }
}

這裏配置了多個訂閱的客戶端。

五、任務積壓監控

生成一個消息,就因為有一個處理該消息的任務要執行,這就導致任務可能出現積壓的情況,常見原因大致有如下幾個:

  • 任務產生的服務過多,任務處理的服務過少,不均衡;
  • 任務處理時間太長,也導致生產過剩;
  • 中間件本身容量偏小,需要擴容或集群化管理;

如果任務積壓過多,可能要對任務生成進行流量控制,或者提升任務的處理能力,從而避免雪崩情況。

六、源代碼地址

GitHub·地址
https://github.com/cicadasmile/data-manage-parent
GitEE·地址
https://gitee.com/cicadasmile/data-manage-parent

推薦閱讀:《架構設計系列》,蘿蔔青菜,各有所需

序號 標題
01 架構設計:單服務.集群.分佈式,基本區別和聯繫
02 架構設計:分佈式業務系統中,全局ID生成策略
03 架構設計:分佈式系統調度,Zookeeper集群化管理
04 架構設計:接口冪等性原則,防重複提交Token管理
05 架構設計:緩存管理模式,監控和內存回收策略

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

【其他文章推薦】

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

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

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

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

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

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

中國礦工秀出用超過 40 台 RTX 3070 筆電挖礦的照片,還有人帶去星巴克偷電挖礦

幾週前我們才報導因顯卡持續缺貨,導致中國加密貨幣礦工看上 RTX 3000 系列筆電的新聞,近日就有中國網友曬出他們用超過 40 台的 RTX 3070 顯卡筆電進行挖礦的照片,非常壯觀,甚至還有人直接把筆電帶到星巴克偷電挖礦,看來 RTX 3000 系列筆電果然也逃不了缺貨的命運了。

中國礦工秀出用超過 40 台 RTX 3070 筆電挖礦的照片

一月初加密貨幣雖然下跌蠻激烈的,尤其是比特幣,從近 42,000 美元的價格跌落到 30,000 美元左右,但經過一個月左右時間的整理,最近又再度上攻,雖然比特幣尚未回到高點,但以太幣早已破前高,而且還持續創高,這也導致挖礦熱潮不退。

稍早一名中國網友就在自己的微博上分享,他們用中國品牌神舟(Hasee)筆電挖礦的照片,挖的是以太幣(ETH),全天 7/24 小時運行,全部皆搭載 NVIDIA GeForce RTX 3070 顯示晶片。而為了確保散熱,他們也使用鐵架,讓每一台都有一大間隔空間,甚至後方窗戶還有一大片的排熱風扇:

有些筆電還像這樣直立擺放:

不過不知道是架子不夠還是怎樣,這張照片就直接疊疊樂,感覺長時間重度運算遲早會燒壞:

RTX 3070 筆電的挖礦效率如何,他就沒有多提,不過從上次另一位網友分享的 RTX 3060 實測,GPU 算力可達到 48.99MH/s,這也代表說 RTX 3070 一定比這高:

也有人問,用筆電挖礦的優勢到底在哪?答案也很簡單,就是還買得到,不像 RTX 3000 系列桌機顯卡缺貨。另外不挖礦時,如果筆電硬體都沒問題,還能賣個二手價:

為了省電費,中國 bilibili 網站上還有一個魚池f2pool 頻道分享她帶到星巴克偷電挖礦的影片,使用的筆電是 Intel i5 + RTX 3060 規格,一整天收益大約 27 人民幣,約台幣 116 元:

完整影片:

如果未來幾天、幾個月加密貨幣再持續上漲,RTX 3000 系列筆電肯定一樣會面臨缺貨窘境,因此有興趣入手的玩家記得手腳要快,等真的沒貨之後就不知道要等多久了。

NVIDIA GeForce RTX 3080 Ti 實測跑分現身,效能表現幾乎跟 RTX 3090 差不多(這張還是有缺陷的工程版)

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

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

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

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

舊 Microsoft Edge 3 月結束支援,微軟開始引導企業過渡到新版

微軟新版 Edge 瀏覽器推出已經一年有餘,而經典款 Edge 結束支援的時間也在逼近,在 2020 年 8 月時,微軟已經預告了將在 2021 年 3 月 9 日結束,雖然對於一般用戶來說影響不大,但對於部分組織來說會是個巨大的變化。現在,該巨頭開始引導企業團體從舊版過渡到 Chromium 版 Edge 上。

舊 Microsoft Edge 3 月結束支援,微軟開始引導企業過渡到新版

在微軟的規劃中,除了 3 月 9 日後不再支援舊版 Edge 外,緊接著在推出 Windows 10 的 4 月更新時還將自動移除設備中的舊版 Edge,倘若你的電腦中還沒有安裝新版 Edge,更新中也將為用戶自動安裝。對於企業團體,微軟開始提供官方建議來幫助組織移除舊版 Edge 瀏覽器。
【部署說明文件,點這裡】

微軟提供的建議重點關係到 Kiosk 模式。對於許多用戶來說舊版 Edge 是目前一線員工和客戶重要的接續點,無論是在負責零售的員工或是需操作重要工具和應用程式的員工,Kiosk 模式能提供一個可客製化的體驗,以幫助用戶能夠完成基本工作。因此,微軟也建議企業能夠在 4 月更新推出前(4 月 13 日)先安裝好新版 Edge 瀏覽器並且部署設定 Kiosk 模式,否則屆時將會出現中斷的情況。

微軟也對此做出解釋。當企業團體切換到新瀏覽器時確實會缺少部分屬於 Kiosk 模式中的功能,但這方面的改進將會與 Microsoft Edge 90 一起推出;另外,該公司也表示預計在 5 月 27 日推出 Edge 91 版時會完整將舊版 Edge 裡面的 Kiosk 模式項目全都過渡到新版瀏覽器上。

◎資料來源:Microsoft、ZDNet

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

※回頭車貨運收費標準

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

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

※推薦評價好的iphone維修中心

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

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

為什麼其他無線藍牙耳塞無法像 Apple AirPods 一樣賣到翻掉?

雖然市面上充斥大量的真無線藍牙耳塞產品,但毫無疑問地,沒有人能夠撼動 Apple AirPods 的地位。在 2020 年 AirPods 佔據了將近一半的市場占比,即使像是 Samsung、小米這樣同樣極具份量的廠牌,就算在智慧型手機的領域銷售驚人,但一跨到真無線藍牙還是得俯首稱臣。到底為什麼 AirPods 這麼受歡迎呢?說是行銷也好,但事實上原因卻更複雜些。

為什麼其他無線藍牙耳塞無法像 Apple AirPods 一樣賣到翻掉?

Apple 在一定程度透過從上至下的一體性封閉控制來成長,使得它比競爭對手更具優勢,其他就是剛好在正確的時機,以及其他產品缺乏創意的競爭。一直以來,評論員們喜歡抨擊 Apple 創造了一個牆內的世界,你很難離開、跨越而不失去常用、實用的功能,甚至整個設備也在範圍之內。舉例來說,你可以把 AirPods  搭配 Android 手機或 Windows 電腦一起使用,但你將會失去 iPhone、 Apple Watch 和 Mac 之間緊密的整合。相比之下與平台毫無關係的真無線藍牙耳塞則提供跨平台同樣功能,甚至讓你可以選用語音助理,從理論上來說,如果你選擇從 Apple 體系跳槽就沒有什麼理由再去擁抱 Apple。

然而也就是這種封閉使得 AirPods 如此具吸引力。Apple 控制著旗下的硬體軟體,始知能夠整合發揮出競爭對手所無法使用或需要額外手續、時間才能做到的功能。例如,Apple 可以說是第一家真正讓用戶可以輕鬆配對與管理真無線藍牙耳塞的公司,藉由輕撫外殼就能在幾秒鐘內讓手機與耳機之間相互連接使用而無需久候。Apple 就是利用這種嚴謹的控制在競爭對手間保持領先地位,Apple 可能並沒有擁有最佳音質、電池續航時間或相容性,但它具有持續設定和提高期望值的更大的優勢,AirPod 通常使用不費力,並且可以依靠軟體作動,Apple 使得其他公司很難在需要支援更廣泛的設備和作業系統時跟上。

雖然時機不是一切,但抓住時機很重要

從歷史紀錄看來,Apple 很少第一個跨足新的設備類別,就像 iPod 絕對不是第一款 MP3 播放器,iPhone 也不是第一部智慧型手機,然而他們卻把握住了即早切入的要點,除了擾亂一波池水,還能從先驅者們的錯誤中吸取教訓,AirPods 就是抓住最佳時機的例子。藍牙耳塞這類產品早在 Apple 於 2016 年推出第一款 AirPods 之前就已經誕生,但用戶數量才剛剛起飛,而且市面上的耳塞也擁有諸多限制,像是電池續航時間短,頸後的連接線和複雜的配對過程。在 AirPods 推出後,將整個過程簡化,也解決了電池續航、外型等通點,並且做到真正的無線。

Apple 的行銷影響力在 AirPods 的成功中扮演了重要角色。相對龐大的尺寸保證了很多人會知道 AirPods 的存在,Apple 透過移除 iPhone 7 上的耳機插孔, 重推了 Airpods 一把。不過,如果 AirPods 來得太晚,或者失去有意義的優勢,這兩項後續的作法都無濟於事,行銷力量只確保 AirPods 或許有最強的開始, 卻不能保證成功。

競爭對手的創意匱乏,只有眾多的致敬品

你可以在市面上找到其他高品質的真無線藍牙耳塞,但更多的是充斥著大量明顯有著 AirPods 影子的耳塞,特別是中國廠商前仆後繼地推出或許外型上存在部分差異,卻怎麼也跳不出 Apple 設計的框架,缺乏真正亮眼、出彩的特色,使得預算較充足的消費者大多不會選擇一個「仿冒品」。

不管獨特性如何,所有的競爭對手都面臨同一個問題:「沒有從根本上改變 Apple  AirPods 的基本概念」。雖然現在市面上其他品項看起來音質更好、續航更長,但卻沒有革命性的技術提升,使得消費者不禁躊躇不前。當 AirPods 在銷售數字上遙遙領先時,這一切都還不夠,雖說這些競爭對手的確為 Android 用戶帶來與 AirPods 一樣的功能,但真正會考慮入手 AirPods 的人,除非預算有限或是真正地深入去進行實驗對照,否則可能不會對其他替代方案產生關注。

從目前整體的市場態勢上來看,要想撼動 AirPods 的地位,可能需要在真無線藍牙耳塞技術上發生重大變化,才比較有可能讓 AirPods 搖兩下,但這或許在很長一段時間裡面並不會發生。

您也許會喜歡:

【推爆】終身$0月租 打電話只要1元/分

立達合法徵信社-讓您安心的選擇

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準