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 架構設計:緩存管理模式,監控和內存回收策略

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

【其他文章推薦】

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

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

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

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

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

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

Tidyverse| XX_join :多個數據表(文件)之間的各種連接

本文首發於公眾號:“生信補給站” Tidyverse| XX_join :多個數據表(文件)之間的各種連接

前面分享了單個文件中的select列filter行列拆分等,實際中經常是多個數據表,綜合使用才能回答你所感興趣的問題。

本次簡單的介紹多個表(文件)連接的方法。

一 載入數據,R包

library(tidyverse)
x <- tribble(
 ~key, ~val_x,
    1, "x1",
    2, "x2",
    3, "x3"
)
y <- tribble(
 ~key, ~val_y,
    1, "y1",
    2, "y2",
    4, "y3"
)

 

二 合併數據

向數據框中加入新變量,新變量的值是另一個數據框中的匹配觀測。

 

1 連接方式

1) 內連接 inner_join

內連接是最簡單的一種連接,只要兩個觀測的鍵是相等的,即可匹配。

 

註釋:匹配在實際的連接操作中是用圓點表示的。圓點的數量 = 匹配的數量 = 結果中行的數量。下同

x %>% 
 inner_join(y, by = "key")
# A tibble: 2 x 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2

內連接最重要的性質是,沒有匹配的行不會包含在結果中。容易丟失觀測,慎用。

 

2) 外連接

外連接則保留至少存在於一個表中的觀測。外連接有 3 種類型: • 左連接 left_join:保留 x 中的所有觀測。 • 右連接 right_join:保留 y 中的所有觀測 • 全連接 full_join:保留 x 和 y 中的所有觀測。

x %>%
left_join(y, by = "key")
# A tibble: 3 x 3
   key val_x val_y
 <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2  
3     3 x3    <NA>
x %>%
right_join(y, by = "key")
# A tibble: 3 x 3
   key val_x val_y
 <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2  
3     4 <NA>  y3
x %>%
full_join(y, by = "key")
# A tibble: 4 x 3
   key val_x val_y
 <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2  
3     3 x3    <NA>
4     4 <NA>  y3

 

 

2 重複鍵

以上均假設鍵具有唯一性,但情況並非總是如此。

如果x中的key變量,在y中有多個同樣的key,那麼所有的結合可能都會羅列出來

x1 <- tribble(
 ~key, ~val_x,
    1, "x1",
    2, "x2",
    2, "x3",
    1, "x4"
)
y1 <- tribble(
 ~key, ~val_y,
    1, "y1",
    2, "y2"
)
left_join(x1, y1, by = "key")
# A tibble: 4 x 3
    key val_x val_y
  <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2  
3     2 x3    y2  
4     1 x4    y1

 

3 定義連接鍵

1) 默認值 by = NULL

使用存在於兩個表中的所有變量,這種方式稱為自然連接。

left_join(x, y)
Joining, by = "key"
# A tibble: 3 x 3
   key val_x val_y
 <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2  
3     3 x3    <NA>

 

2) 定義匹配鍵 by = c("a" = "b")

匹配 x 表中的 a 變量和 y 表中的 b 變量,輸出結果中使用的是 x 表中的變量。

y_1 <- tribble(
 ~key2, ~val_y,
    1, "y1",
    2, "y2"
)
left_join(x, y_1, by = c("key" = "key2"))
# A tibble: 3 x 3
   key val_x val_y
 <dbl> <chr> <chr>
1     1 x1    y1  
2     2 x2    y2  
3     3 x3    <NA>

 

3) 多個匹配鍵

x2 <- tribble(
 ~key,~key1, ~val_x,
    1, 2018,"x1",
    2, 2019,"x2",
    3, 2019,"x3"
)
y2 <- tribble(
 ~key, ~key1,~val_y,
    1, 2018,"y1",
    2, 2018,"y2",
    4, 2019,"y3"
)
inner_join(x2,y2,by = c("key","key1"))
# A tibble: 1 x 4
   key  key1 val_x val_y
 <dbl> <dbl> <chr> <chr>
1     1  2018 x1    y1  

 

三 篩選連接

篩選連接匹配觀測的方式與合併連接相同,但前者影響的是觀測,而不是變量。篩選連接 有兩種類型。

semi_join函數
  • 保留 x 表中與 y 表中的觀測相匹配的所有觀測

semi_join(x, y, by = "key")
# A tibble: 2 x 2
   key val_x
 <dbl> <chr>
1     1 x1  
2     2 x2

 

anti_join函數
  • 丟棄 x 表中與 y 表中的觀測相匹配的所有觀測。

 

anti_join(x, y, by = "key")
# A tibble: 1 x 2
   key val_x
 <dbl> <chr>
1     3 x3

 

參考資料:

https://r4ds.had.co.nz/

《R數據科學》

 

【覺得不錯,右下角點個“在看”,期待您的轉發,謝謝!】

 

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

【其他文章推薦】

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

※台北網頁設計公司全省服務真心推薦

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

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

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

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

面試三輪我倒在了一道sql題上——sql性能優化

一、前言

最近小農在找工作,因為今年疫情的特殊原因,導致工作不是特別好找,所以一旦有面試電話,如果可以,都會去試一試,剛好接到一個面試邀請,感覺公司還不錯,於是就確定了面試時間,準備了一下就去面試了。

第一輪面試是小組組長面試,通過。
第二輪是經理面試也是通過了。
第三輪總監面試,前面都還有模有樣,突然畫風一轉,面試官說:“問你最後一個問題”

面試官:10W條數據,我要從其中查出100條不連續的數據,給你id,來查name和password進行展示,如何才能高性能的去使用?

我:在id上建立聚簇索引,然後用 in id 來縮小表搜索範圍,最後 使用條件查詢 小於最大id,大於最小id,這樣可以讓sql速度能夠比較快的展示,雖然In的性能比較低
心裏活動:雕蟲小技,還最後一個問題,這樣的問題再來一個吧

只見面試官緊鎖眉頭,與我心裏期待的表情有點不一樣啊,難道是哪個環節出了問題?
面試官:這樣的性能不能達到最優化的程度,而且如果我給你的最小id是1,最大id是100000呢?

你這就有點杠精了啊,那行吧,你是面試官你說了算
我:既然id已經給出來了,而且只查詢兩個字段,用聚簇索引那麼查詢數據是很快的,用in id應該是可以的。

面試官:好的,回去等通知吧
我。。。。。

二、後知

於是回去后,查詢資料,才知道原來面試官,真正想考的是 “覆蓋索引”

什麼是覆蓋索引:

當sql語句的所求查詢字段(select列)和查詢條件字段(where子句)全都包含在一個索引中 (聯合索引),可以直接使用索引查詢而不需要回表。這就是覆蓋索引,通過使用覆蓋索引,可以減少搜索樹的次數,這就是 覆蓋索引,在了解覆蓋索引之前,我們先來看看什麼是索引。

三、什麼是索引?

我們有一個主鍵列為id的表,表中有字段name,並且在name上有索引

表中 t_user 值分別為(1,張一)、(2,張二)、(3,張三)、(4,張四)、(5,張五)

表結構如下:

mysql> create table t_user (
id bigint(20) not null auto_increment ,
name varchar(255) not null,
primary key (id),
index index_name (name) using btree)
engine=innodb
default character set=utf8 collate=utf8_general_ci

兩棵樹的示例示意圖如下:

從圖中不難看出,根據恭弘=叶 恭弘子節點的內容,索引類型分為主鍵索引和二級索引(非主鍵索引)。

主鍵索引: 主鍵索引的恭弘=叶 恭弘子節點保存着主鍵即對應行的全部數據。在InnoDB里,主鍵索引也被稱為聚簇索引(clustered index)。

二級索引(非主鍵索引): 二級索引樹中的恭弘=叶 恭弘子結點保存着索引值和主鍵值,當使用二級索引進行查詢時,需要進行回表操作。在InnoDB里,非主鍵索引也被稱為二級索引(secondary index)

通過上面所講的,我們來看看如何通過sql語句來區分 主鍵索引和普通索引的查詢

  • select * from t_user where id=1 即主鍵查詢方式,則只需要搜索id這棵B+樹
  • select * from t_user where name=張三 即普通索引查詢方式,則需要先搜索name索引樹,得到id的值為3,再到id索引樹搜索一次。這個過程稱為回表

也就是說,基於二級索引(非主鍵索引)的查詢需要多掃描一棵索引樹。因此,我們在應用中應該盡量使用主鍵查詢。

看到這裏如果你看懂了上面的介紹,那麼這裏你會有一個疑問,我直接用in id不就好了嗎,建立id主鍵索引,就可以不用回表了,速度不也就提升了嗎?

如果是 5.5 之前的版本確實不會走索引的,在 5.5 之後的版本,MySQL 做了優化。MySQL 在 2010 年發布 5.5 版本中,優化器對 in 操作符可以自動完成優化,針對建立了索引的列可以使用索引,沒有索引的列還是會走全表掃描,也就是我們所說的回表。

那麼,有沒有可能經過索引優化,避免回表過程呢?答應是有的

四、覆蓋索引

sql語句如下,其中id自增,name為索引:

mysql> create table t_user (
id bigint(20) not null auto_increment ,
name varchar(255) not null,
password varchar(255) ,
primary key (id),
engine=innodb
default character set=utf8 collate=utf8_general_ci

比如有這麼兩句sql

語句A: select id from user_table where name= '張三'
語句B: select password from user_table where name= '張三'

語句A: 因為 name索引樹 的恭弘=叶 恭弘子結點上保存有 name和id的值 ,所以通過 name索引樹 查找到id后,因此可以直接提供查詢結果,不需要回表,也就是說,在這個查詢裏面,索引name 已經 “覆蓋了” 我們的查詢需求,我們稱為 覆蓋索引

語句B: name索引樹 上 找到 name=’張三’ 對應的主鍵id, 通過回表在主鍵索引樹上找到滿足條件的數據

因此我們可以得知,當sql語句的所求查詢字段(select列)和查詢條件字段(where子句)全都包含在一個索引中(聯合索引),可以直接使用索引查詢而不需要回表。這就是覆蓋索引

例如上面的語句B是一個高頻查詢的語句,我們可以建立(name,password)的聯合索引,這樣,查詢的時候就不需要再去回表操作了,可以提高查詢效率。

所以關於上面的面試題我們就可以得出,使用聯合索引就可以很好的回答面試官的問題(id,name,password)這樣的聯合索引就可以調用到覆蓋索引,可以減少樹的搜索次數,不再需要回表查整行記錄,顯著提升查詢性能,所以使用覆蓋索引是一個常用的性能優化手段。

說到了聯合索引我們就不得不說聯合索引中最重要的匹配原則,最左匹配原則了

五、最左匹配原則

最左前綴匹配原則,是非常重要的原則,mysql會從左向右進行匹配。

例如我們定義了(name,password)兩個聯合索引字段,我們 使用 where name = '張三' and password = '2' 索引可以生效的,當我們是顛倒了他們的順序 使用where password = '1' and name = '王五',索引同樣也是可以生效的,在mysql查詢優化器會判斷糾正這條sql語句該以什麼樣的順序執行效率最高,最後才生成真正的執行計劃,我們能盡量的利用到索引時的查詢順序效率最高,所以mysql查詢優化器會最終以這種順序(where name = '張三' and password = '2' )進行查詢執行,就類似 我們的 order by name,password這樣一種排序規則,先對張三的用戶進行查詢排序,在對password進行處理

比如我們要查詢姓張的用戶,我們的條件查詢可以為 "where name like ‘張%’",但是不能是 where name like '%張%'或者是 where name like '%張',因為索引可以用於查詢條件字段為索引字段,根據字段值必須是最左若干個字符進行的模糊查詢,也就是需要是 ‘張%’ 這樣的添加才可以使用。

索引的復用能力。因為可以支持最左前綴,所以當已經有了(name,password)這個聯合索引后,一般就不需要單獨在name上建立索引了。因此,第一原則是,如果通過調整順序,可以少維護一個索引,那麼這個順序往往就是需要優先考慮採用的。

如果既有聯合查詢,又有基於name,password各自的查詢呢?查詢條件裏面只有password的語句,是無法使用(name,password)這個聯合索引的,這時候你需要同時維護(name,password)、(password) 這兩個索引。

創建索引時,我們也要考慮空間代價,使用較少的空間來創建索引
假設我們現在不需要通過name查詢password了,需要通過name查詢age或通過age查詢name

  • 1.(name,age)聯合索引+age單字段索引
  • 2.(age,name)聯合索引+name單字段索引

name字段是比age字段大的,所以,選擇第一種,索引佔用空間較小的一個

六、索引下推

上面我們說到滿足最左前綴原則的時候,最左前綴可以用於在索引中定位記錄。那麼如果那些不符合最左前綴的部分,會怎麼樣呢?

如果現在有一個需求:檢索出表中“名字第一個字是張,而且沒有刪除的信息(is_del = 1)。SQL語句如下:

mysql> select * from t_user where name like ‘張%’ and is_del=1

在MySQL 5.6之前,只能從匹配的位置一個個回表。到主鍵索引上找出數據行,再對比字段值

在MySQL 5.6中 引入的索引下推優化(index condition pushdown), 可以在索引遍歷過程中,對索引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數

根據(username,is_del)聯合索引查詢所有滿足名稱以“張”開頭的索引,然後回表查詢出相應的全行數據,然後再篩選出未刪除的用戶數據。過程如下圖:

每一個虛線箭頭表示回表一次
圖一(無索引下推執行流程)

每一個虛線箭頭表示回表一次
圖二(索引下推執行流程)

圖1跟圖2的區別是,InnoDB在(name,is_del)索引內部就判斷了數據是否邏輯刪除,對於邏輯刪除的記錄,直接判斷並跳過。在我們的這個例子中,只需要對ID1、ID4這兩條記錄回表取數據判斷,就只需要回表2次

mysql默認啟用索引下推,我們也可以通過修改系統變量optimizer_switch的index_condition_pushdown標誌來控制SET optimizer_switch = 'index_condition_pushdown=off';

我們也需要注意:

  • innodb引擎的表,索引下推只能用於二級索引,因為innodb的主鍵索引樹恭弘=叶 恭弘子結點上保存的是全行數據,所以這個時候索引下推並不會起到減少查詢全行數據的效果
  • 索引下推一般可用於所求查詢字段(select列)不是/不全是聯合索引的字段,查詢條件為多條件查詢且查詢條件子句(where/order by)字段全是聯合索引

六、小結

今天的內容就到這裏了,我們在上面描述了數據庫索引的概念,包括了覆蓋索引、聯合索引、索引下推,那麼下次如果有面試官問你剛開始的問題,相信大家可以好好的回(dui)答(ta)一下面試官了,在sql優化中,減少回表次數,或者直接使用覆蓋索引是比較重要的,盡量少地訪問資源也是數據庫設計的重要原則之一,謝謝大家,加油~

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

【其他文章推薦】

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

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

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

※回頭車貨運收費標準

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

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

中國礦工秀出用超過 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維修中心

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

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

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

小米11 Pro 新外觀渲染圖、規格曝光?!小米11 系列還有兩款高階機型尚未推出,不過小米10 將有 S870 小改升級版本

除了幾天後將舉行的小米11 全球發表會,屆時預計最快在三月間台灣消費者也有機會購入小米11 旗艦新機。而小米11 系列其實還有包括「大杯」的小米11 Pro 和「超大杯」的小米11 Pro+(或稱為小米11 Ultra)還沒有正式亮相。不過在小米的「友商」Redmi 已經確定將在本月發表全新 Redmi K40 系列新機後,預期這兩款未推出的小米11 系列高階機型得等到三月才會正式揭曉。

▲圖片來源:@xiaomiui(Telegram)

小米11 Pro 新外觀渲染圖、規格曝光?!小米11 系列還有兩款高階機型尚未推出,不過小米10 將有 S870 小改升級版本

從 @xiaomiui 這 Telegram 爆料的小米11 Pro 渲染圖,在主相機設計和小米11 並沒有太大不同,最顯著的差異在閃光燈改設計在主鏡頭的環形 LED 補光燈設計。
規格方面,小米11 Pro、小米11 Pro+預計搭載 6.81 吋 2K 解析度、120Hz 螢幕更新率、採用三星 E4 發光材料的 SuperAMOLED 曲面螢幕,在螢幕規格方面大致和小米11 標準版保持一致。硬體規格方面,則同樣搭載高通 Snapdragon 888 處理器、 LPDDR5 RAM 和 UFS3.1 ROM。
相機部份則是這三款小米11 之間最顯著的差異,其中小米11 Pro 預計搭載 5000 萬像素(Samsung ISOCELL GN2 感光元件)主鏡頭、4800 萬像素超廣角鏡頭和 4800 萬像素 5x 潛望式長焦鏡頭(支持 120x 混合變焦拍照、 15x 變焦錄影)。
其他方面,傳聞電池內建等效 5000mAh 容量大電池,其中最高規的小米11 Pro+(小米11 Ultra)預計支持最高 120W 有線快充、67W 無線快充。

▲圖片來源:@xiaomiui(Telegram)

不過回顧數碼閒聊站之前在微博提到小米11 Pro 的主相機矩陣模組比起之前更大、辨識度更高,因此這次 @xiaomiui 釋出的渲染圖其實可信度並不高。
據傳小米近期也將推出多款新機,除了幾天後小米11 將在全球發表,接下來小米10 系列將帶來更新處理器的新版本、小米11 系列則有小米11 Pro 和小米11 Ultra(或稱小米11 Pro+) ,至於 Redmi K40 系列以及 Redmi 遊戲手機預計在本月下旬也會亮相。

▲圖片來源:數碼閒聊站(微博)

相比之下,其實上個月 Ben Geskin 根據傳聞製作的小米11 Pro 概念渲染圖外觀或許還比較接近未來小米11 Pro 真正的樣貌:

▲圖片來源:Ben Geskin(Twitter/ @BenGeskin)

至於數碼閒聊站之前微博提到小米10 有款神秘新機,日前也表示小米10 新版本通過認證,和之前搭載高通 Snapdragon 865 處理器的小米10 之間最大的差異在於升級為高通 Snapdragon 870 ,其餘規格則相同。另外,從近期小米官方、雷軍等人微博的貼文動態已多次提及小米10 來看,確實在不久後可能帶來這款小幅升級的全新小米10 。

▲圖片來源:數碼閒聊站(微博)

消息來源:@xiaomiui(Telegram)

延伸閱讀:
華碩傳聞將在今年推出小尺寸旗艦 ZenFone mini , ROG Phone 5 於 4 月推出、價格將高於 ROG Phone 3

小米首款「四曲瀑布螢幕概念手機」正式亮相!四邊 88° 超曲面螢幕、螢幕下前鏡頭、整機一體無孔化設計

您也許會喜歡:

【推爆】終身$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元/分

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

※回頭車貨運收費標準