国产亚洲精品福利在线无卡一,国产精久久一区二区三区,亚洲精品无码国模,精品久久久久久无码专区不卡

當(dāng)前位置: 首頁 > news >正文

手機企業(yè)網(wǎng)站制作企業(yè)網(wǎng)頁設(shè)計公司

手機企業(yè)網(wǎng)站制作,企業(yè)網(wǎng)頁設(shè)計公司,搜索引擎優(yōu)化搜索優(yōu)化,網(wǎng)站客服圖標(biāo)目錄 Redis 是單線程的!為什么 Redis-Key(操作redis的key命令) String 擴展字符串操作命令 數(shù)字增長命令 字符串范圍range命令 設(shè)置過期時間命令 批量設(shè)置值 string設(shè)置對象,但最好使用hash來存儲對象 組合命令getset,先get然后在set Hash hash命令: h…

目錄

Redis 是單線程的!為什么

Redis-Key(操作redis的key命令)

String

擴展字符串操作命令

數(shù)字增長命令

字符串范圍range命令

設(shè)置過期時間命令

批量設(shè)置值

string設(shè)置對象,但最好使用hash來存儲對象

組合命令getset,先get然后在set

Hash

hash命令:

hash基礎(chǔ)的增刪查

hash擴展命令

總結(jié)

List

Set

ZSet

Redis事務(wù)

redis的事務(wù)命令:

正常執(zhí)行事務(wù) exec

放棄事務(wù) discard

命令異常,事務(wù)回滾

1.類似于java編譯型異常(語法錯誤)

2.類似于java運行時異常(如1/0,除數(shù)不能為0錯誤) (邏輯錯誤)

監(jiān)控 Watch(相當(dāng)于java加鎖) (面試: redis的watch命令監(jiān)控實現(xiàn)秒殺系統(tǒng))

悲觀鎖

樂觀鎖

Redis用watch做樂觀鎖實現(xiàn)步驟

redis監(jiān)視watch測試案例

取錢正常執(zhí)行成功的流程

取錢出現(xiàn)并發(fā)問題的流程

Redis的Java客戶端

測試jedis

Springboot整合Redis

自定義RedisTemplate

緩存

緩存穿透

解決方案一:緩存空數(shù)據(jù)

解決方案二:布隆過濾器

緩存擊穿

解決方案一:互斥鎖(分布式鎖)

解決方案二:邏輯過期

緩存雪崩

持久化

1.RDB

RDB執(zhí)行原理

2.AOF

RDB與AOF對比

雙寫一致性

1.一致性要求高

1.延遲雙刪

1.無論第一步是先刪除緩存還是先修改數(shù)據(jù)庫都會導(dǎo)致臟數(shù)據(jù)的出現(xiàn)

2.刪除兩次緩存的原因

3.延時刪除的原因

總結(jié)

2.加讀寫鎖

2.允許延遲一致(較為主流)

1.異步通知保證數(shù)據(jù)的最終一致性

2.基于Canal的異步通知

數(shù)據(jù)過期策略

方案一惰性刪除:

方案二定期刪除:

總結(jié)

數(shù)據(jù)淘汰策略

八種不同策略

數(shù)據(jù)淘汰策略--使用建議

分布式鎖

場景

引入與基本介紹

redis分布式鎖實現(xiàn)原理

基本介紹

設(shè)置超時失效時間的原因(避免死鎖):

總結(jié)

缺陷

Redisson實現(xiàn)的分布式鎖

1.redisson實現(xiàn)的分布式鎖的執(zhí)行流程/合理地控制鎖的有效時長(失效時間)

2.可重入

3.主從一致性

Redis發(fā)布訂閱

訂閱/發(fā)布消息圖

命令

測試

Redis發(fā)布訂閱原理

總結(jié)

使用場景

Redis消息隊列

概念

基于List結(jié)構(gòu)模擬消息隊列(可實現(xiàn)阻塞隊列的效果)

基于PubSub(發(fā)布訂閱)的消息隊列

基于Stream的消息隊列

基本知識

消費者組

Redis集群(分布式緩存)

單點Redis的問題

主從復(fù)制

主從數(shù)據(jù)同步原理

1.主從全量同步

2.主從增量同步(slave重啟或后期數(shù)據(jù)變化)

3.總結(jié)

哨兵模式

哨兵模式的結(jié)構(gòu)與作用

服務(wù)狀態(tài)監(jiān)控

哨兵選主規(guī)則(主節(jié)點宕機后,選從節(jié)點為主節(jié)點的規(guī)則)

Redis集群(哨兵模式)的腦裂問題

分片集群

redis集群環(huán)境部署(環(huán)境配置)

一主二從集群搭建(命令或文件配置)(這種方式的redis集群實際工作用不到,僅供基礎(chǔ)學(xué)習(xí))

命令方式配置

文件方式配置(一主二從,持久化的,對于哨兵模式,不建議使用這種)

一主兩從的第二種搭建方式(層層鏈路)哨兵模式的手動版

I/O多路復(fù)用模型

redis為什么這么快

用戶空間和內(nèi)核空間

阻塞IO

非阻塞IO

IO多路復(fù)用

Redis網(wǎng)絡(luò)模型


Redis 是單線程的!為什么

Redis是基于內(nèi)存實現(xiàn)的,使用Redis時,CPU幾乎不會成為Redis性能瓶頸,Redis的瓶頸是機器的內(nèi)存網(wǎng)絡(luò)帶寬(網(wǎng)絡(luò)),既然可以使用單線程來實現(xiàn),就使用單線程了!所有就使用了單線程了!

內(nèi)存訪問速度:由于Redis將數(shù)據(jù)存儲在內(nèi)存中,數(shù)據(jù)訪問速度非???/strong>,通常接近于CPU的緩存訪問速度。這意味著CPU在讀取或?qū)懭霐?shù)據(jù)時很少需要等待,從而減少了CPU的空閑時間。

計算密集度:Redis的操作通常是簡單的數(shù)據(jù)查找、插入、刪除和計算集合操作(如交集、并集等)。這些操作在CPU層面上的計算復(fù)雜度相對較低,因此不太可能使CPU成為瓶頸。

并發(fā)處理:Redis使用單線程模型來處理客戶端請求(盡管有IO多路復(fù)用技術(shù)來同時處理多個連接),但這并不意味著Redis不能利用多核CPU。通過部署多個Redis實例或使用Redis集群,可以水平擴展以利用多核CPU的并行處理能力。

Redis 是C 語言寫的,官方提供的數(shù)據(jù)為 100000+ 的QPS,完全不比同樣是使用 key-vale的Memecache差!

Redis 為什么單線程還這么快?

1、高性能的服務(wù)器不一定是多線程的

2、多線程不一定比單線程效率高!(多線程CPU上下文會切換消耗資源!)

速度:CPU>內(nèi)存>硬盤

核心:redis 是將所有的數(shù)據(jù)全部放在內(nèi)存中的,所以說使用單線程去操作效率就是最高的,多線程(CPU上下文會切換:耗時的操作!

對于內(nèi)存系統(tǒng)來說,如果沒有上下文切換效率就是最高的!

多次讀寫都是在一個CPU上的,在內(nèi)存情況下,這個就是最佳的方案!

Redis-Key(操作redis的key命令)

keys * # 查看所有的key?

set name123 kuangshen # set key

EXISTS name123 # 判斷當(dāng)前的key是否存在

move name123 # 移除當(dāng)前的key

EXPIRE name123 10 # 設(shè)置key的過期時間,單位是秒

ttl name123 # 查看當(dāng)前key的剩余時間

type name123? ??# 查看當(dāng)前key的一個類型

String

擴展字符串操作命令
  • APPEND key123 "hello" # 追加字符串,如果當(dāng)前key不存在,就相當(dāng)于setkey?
  • STRLEN key123 # 獲取字符串的長度!
數(shù)字增長命令
  • set views123 0 # 初始瀏覽量為0?
  • incr views123 # 自增1 瀏覽量變?yōu)??
  • decr views123 # 自減1 瀏覽量-1?
  • INCRBY views123 10 # 可以設(shè)置步長,指定增量10!
字符串范圍range命令
  • GETRANGE key123 0 3 # 截取字符串 [0,3]
  • GETRANGE key123 0 -1 # 獲取全部的字符串 和 get key是一樣的
  • SETRANGE key123 1 xx # 替換指定位置1開始的字符串!
設(shè)置過期時間命令

setex (set with expire) //設(shè)置過期時間

例:setex key123 30 "hello" //設(shè)置key123 的值為hello,30秒后過期

setnx (set if not exist) //key不存在在設(shè)置,key存在則回滾設(shè)置失敗(在分布式鎖中會常常使用)?

例:setnx mykey123 "MongoDB" //如果mykey不存在創(chuàng)建成功,存在,創(chuàng)建失敗不會替換值

批量設(shè)置值
  • ?mset k1 v1 k2 v2 k3 v3 # 同時設(shè)置多個key和value值(k1:v1, k2:v2, k3:v3)
  • mget k1 k2 k3 # 根據(jù)多個key同時獲取多個值
  • msetnx k1 v1 k4 v4 # msetnx 是一個原子性的操作,要么一起成功,要么一起失敗!
    • 由于k1已經(jīng)存在,所以setnx一定會失敗,由于是原子性操作k4也會跟著失敗
string設(shè)置對象,但最好使用hash來存儲對象
  • set user:1 {name:zhangsan,age:3} # 設(shè)置一個key為user:1的對象,值為 json字符來保存一個對象!?
    • key值為user:{id}, value值為json
  • mset user:1:name zhangsan user:1:age 2 #批量創(chuàng)建對象
    • 這里的key是一個巧妙的設(shè)計: user:{id}:{filed} , 如此設(shè)計在Redis中是完全OK了!
  • mget user:1:name user:1:age #批量獲取對象中的值
組合命令getset,先get然后在set
  • getset db redis # 如果不存在值,則返回 nil?,但同時值被設(shè)置成了redis
  • getset db mongodb # 如果存在值,獲取原來的值,并設(shè)置新的值

Hash

相當(dāng)于Map集合,key-value!,只是value存的是map,也就是key-map,值是map集合

  • hash本質(zhì)和String類型沒有太大區(qū)別,還是一個簡單的key-value
  • hset myhash field codeyuaiiao
    • 命令含義:hset key mapkey mapvalue

hash命令:

hash基礎(chǔ)的增刪查
  • hset myhash field1 yuaiiao # 添加或修改一個具體的值
  • hget myhash field1 # 獲取一個字段值
  • hmset myhash field4 hello field5 byebye # 添加多個字段值進map集合
  • hmget myhash field3 field4 # 獲取多個指定字段值
  • hgetall myhash # 獲取hash全部字段值(包含了key和value)
  • hdel myhash field1 field2 # 刪除一個或多個指定字段值
hash擴展命令
  • hlen myhash # 獲取hash表的字段數(shù)量
  • hexists myhash field1 # 判斷hash表中指定字段是否存在
  • hkeys myhash # 獲取所有field(相當(dāng)于key)
  • hvals myhash # 獲取所有value
  • hincrby myhash field3 2 # 指定增量
  • hincrby myhash field3 -2 # 指定減量
  • hsetnx myhash field4 yuaiiao # 如果不存在可以設(shè)置,如果存在則不可設(shè)置

總結(jié)

hash變更的用戶數(shù)據(jù)user表,name,age字段 ,尤其是用戶信息之類的,經(jīng)常變動的信息!

hash更適合于對象的存儲,String更加適合字符串存儲

List

Set

Redis的Set結(jié)構(gòu)與java中的HashSet類似,可以看做是一個value為null的HashMap.因為也是一個hash表,因此具備與hashset類似的特征:
無序
元素不可重復(fù)
查找快
支持交集,并集,差集等功能

ZSet

Redis事務(wù)

mysql事務(wù)本質(zhì)

  • ACID特性,要么同時成功,要么同時失敗,保證原子性!

Redis事務(wù)本質(zhì)

  • 一組命令的集合! 一個事務(wù)中的所有命令都會被序列化,在事務(wù)執(zhí)行過程中,會按照順序執(zhí)行!
  • 一次性,順序性,排他性! 執(zhí)行一系列的命令!
------隊列 set get set 執(zhí)行-----
  • Redis 事務(wù)沒有隔離級別的概念!
    • 不會出現(xiàn)幻度,臟讀,不可重復(fù)讀等問題
  • 所有的命令在事務(wù)中,并沒有直接被執(zhí)行,只有發(fā)起執(zhí)行命令的時候才會執(zhí)行! Exec
  • Redis單條命令是保證原子性的,但是redis事務(wù)是一組命令的集合,所以不保證原子性!

redis的事務(wù)命令:

  • 開啟事務(wù)(multi)
  • 命令入隊(要執(zhí)行的命令) (事務(wù)隊列)
  • 執(zhí)行事務(wù)(exec)

正常執(zhí)行事務(wù) exec

127.0.0.1:6379> multi # 開啟事務(wù)
OK
127.0.0.1:6379> set key hello # 執(zhí)行命令
QUEUED
127.0.0.1:6379> set key1 yuaiiao
QUEUED
127.0.0.1:6379> get key
QUEUED
127.0.0.1:6379> get key1
QUEUED
127.0.0.1:6379> exec # 執(zhí)行事務(wù)
1) OK
2) OK
3) "hello"
4) "yuaiiao"
127.0.0.1:6379> 

放棄事務(wù) discard

127.0.0.1:6379> multi # 開啟事務(wù)
OK
127.0.0.1:6379> set key 1
QUEUED
127.0.0.1:6379> set key2 3
QUEUED
127.0.0.1:6379> discard # 放棄事務(wù)
OK
127.0.0.1:6379> get key2 #事務(wù)隊列中的命令都不會被執(zhí)行
(nil)
127.0.0.1:6379> 

命令異常,事務(wù)回滾

1.類似于java編譯型異常(語法錯誤)

命令語法導(dǎo)致執(zhí)行錯誤,事務(wù)中所有的命令都不會被執(zhí)行。

127.0.0.1:6379> multi # 開啟事務(wù)
OK
127.0.0.1:6379> set k1 v1 # 執(zhí)行命令
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> getset k3 v3
QUEUED
127.0.0.1:6379> getset k3 # 錯誤的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec # 執(zhí)行事務(wù)報錯
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2 # 事務(wù)執(zhí)行失敗,得不到值,所有的命令都不會被執(zhí)行
(nil)
127.0.0.1:6379> 
2.類似于java運行時異常(如1/0,除數(shù)不能為0錯誤) (邏輯錯誤)

命令邏輯執(zhí)行錯誤 , 那么執(zhí)行命令的時候,其他的命令是可以正常執(zhí)行的,只是錯誤命令拋出異常!

證明事務(wù)不保證原子性

127.0.0.1:6379> set k1 "v1"  # 字符串
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> incr k1 # 對字符串進行 自增1 運行時異常錯誤
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec # 錯誤的命令報錯但是其余命令都能執(zhí)行
1) (error) ERR value is not an integer or out of range 
2) OK
3) OK
4) "v2"
127.0.0.1:6379> get k2 # 其余命令正常執(zhí)行
"v2"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379> 

監(jiān)控 Watch(相當(dāng)于java加鎖) (面試: redis的watch命令監(jiān)控實現(xiàn)秒殺系統(tǒng))

悲觀鎖

很悲觀,認為什么時候都會出問題,無論做什么都會加鎖,效率低下

樂觀鎖

很樂觀,認為什么時候都不會出現(xiàn)問題,所以不會上鎖,更新數(shù)據(jù)的時候去判斷一下,在此期間是否有人修改過這個數(shù)據(jù),使用version字段比較。

Redis用watch做樂觀鎖實現(xiàn)步驟

1.獲取最新version

2.更新的的時候比較version,version沒變更新成功,version改變進入自旋。

redis的樂觀鎖watch

  • watch加鎖,記得用完需要unwatch解鎖
redis監(jiān)視watch測試案例
取錢正常執(zhí)行成功的流程
127.0.0.1:6379> set money 100 #存錢100
OK
127.0.0.1:6379> set out 0 #取錢0
OK
127.0.0.1:6379> watch money # 監(jiān)視money對象
OK
127.0.0.1:6379> multi 
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 事務(wù)正常結(jié)束 , 期間數(shù)據(jù)沒有發(fā)生變動 ,這個時候就正常執(zhí)行成功了!
1) (integer) 80
2) (integer) 20
127.0.0.1:6379> 
取錢出現(xiàn)并發(fā)問題的流程
  • 測試多線程修改值,使用watch當(dāng)做redis 的樂觀鎖操作
    • 在開一個redis-client客戶端,一共有兩個客戶端
    • 客戶端1:開啟事務(wù),money取錢20
    • 客戶端2:這時候直接把money修改成1000
    • 客戶端1:繼續(xù)執(zhí)行,out存錢20,這時候執(zhí)行事務(wù)會
      • 執(zhí)行返回nil,修改失敗
# 客戶端1:開啟事務(wù),監(jiān)視money對象,money取錢20
127.0.0.1:6379> watch money # 監(jiān)視money對象
OK
127.0.1:6379> multi 
OK
127.0.0.1:6379> decrby money 20 
QUEUED# 客戶端1還未提交,客戶端2:這時候直接把money修改成1000
# set money 10000# 客戶端1:繼續(xù)執(zhí)行,out存錢20,然后執(zhí)行事務(wù),樂觀鎖對比money版本號改動了,執(zhí)行失敗
# 執(zhí)行之前,另一個線程,修改了我們的值,就會導(dǎo)致事務(wù)執(zhí)行失敗!#  127.0.0.1:6379> get money"80"#  127.0.0.1:6379> set money 1000#  OK
127.0.0.1:6379> incrby out 20 #out存錢20
QUEUED
127.0.0.1:6379> exec # 提交事務(wù),監(jiān)視money的version是否變化,有變化事務(wù)回滾,結(jié)果返回nil
(nil)
127.0.0.1:6379> 
  • redis事務(wù)執(zhí)行失敗后的自旋步驟
    • 釋放監(jiān)控鎖watch,在重新獲取鎖重復(fù)以上步驟,進行自旋
127.0.0.1:6379> unwatch # 釋放鎖(監(jiān)控),如果發(fā)現(xiàn)事務(wù)執(zhí)行失敗,就先解鎖
OK
127.0.0.1:6379> watch money # 重新獲取鎖,獲取最新的值,再次監(jiān)視,select version
OK
127.0.0.1:6379> multi # 開啟事務(wù)執(zhí)行正常操作
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec # 在比對監(jiān)視的值是否發(fā)生了變化,如果沒有變化,可以執(zhí)行成功呢,如果變化了就執(zhí)行失敗,在重新以上步驟
1) (integer) 980
2) (integer) 40
127.0.0.1:6379> 
  • 如果修改失敗,獲取最新的值就好

Redis的Java客戶端

我們要使用java來操作redis

有springboot整合了,我們也要學(xué)習(xí)jedis

什么是jedis?

  • jedis 是redis官方推薦的java連接開發(fā)工具! 使用java操作Redis的中間件 ! 如果你要使用java 操作redis, 那么一定要對jedis十分熟悉

測試jedis

  • 導(dǎo)入對應(yīng)的依賴
<!--導(dǎo)入jedis-->
<dependencies><!-- https://mvnrepository.com/artifact/redis.clients/jedis --><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.3.0</version></dependency><!--導(dǎo)入 fastjson--><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.70</version></dependency>
</dependencies>

2.編碼測試

  • 連接redis數(shù)據(jù)庫
  • 操作命令
  • 斷開連接
package com.kuang;
import redis.clients.jedis.Jedis;public class TestPing {public static void main(String[] args) {// 1、 new Jedis 對象即可Jedis jedis = new Jedis("127.0.0.1", 6379);// jedis 所有的命令就是我們之前學(xué)習(xí)的所有指令!所以之前的指令學(xué)習(xí)很重要!System.out.println(jedis.ping());}
}

結(jié)果輸出:

jedis 所有的命令就是我們之前學(xué)習(xí)的所有指令!所以之前的指令學(xué)習(xí)很重要!

Springboot整合Redis

說明 :在 SpringBoot2.x之后, 原來使用的jedis 被替換為了 lettuce ?

    • jedis :底層采用的直連, 多個線程操作的話 ,是不安全的, 如果想要避免不安全, 使用jedis pool 連接池 ! 更像 BIO模式,阻塞的.
    • lettuce : 采用netty , 實例可以再多個線程中進行共享,不存在線程不安全的情況 ! 可以減少線程數(shù)量,不需要開連接池, 更像NIO模式非阻塞的

自定義RedisTemplate

redis關(guān)于對象的保存,對象需要序列化。

  • 對象如果不序列化保存,則會報錯

分析redisTemplate源碼為什么對象需要序列化

  • 分析源碼序列化配置

新建config/RedisConfig

  • 不使用JDK序列化,key使用哪個string序列化,value使用json序列化
package com.kuang.config;import com.fasterxml.jackson.annotation.JsonAutoDetect; 
import com.fasterxml.jackson.annotation.PropertyAccessor; 
import com.fasterxml.jackson.databind.ObjectMapper; 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory; 
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {// 這是我給大家寫好的一個固定模板,大家在企業(yè)中,拿去就可以直接使用!// 自己定義了一個 RedisTemplate @Bean @SuppressWarnings("all")public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {// 我們?yōu)榱俗约洪_發(fā)方便,一般直接使用 <String, Object>//源碼是<Object,Object>類型,可以自定義把Object轉(zhuǎn)換成String類型RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();template.setConnectionFactory(factory);// Json序列化配置Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 	om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// String 的序列化,解決redis存儲字符串是轉(zhuǎn)義字符,看著像亂碼StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();// key采用String的序列化方式template.setKeySerializer(stringRedisSerializer);// hash的key也采用String的序列化方式template.setHashKeySerializer(stringRedisSerializer);// value 序 列 化 方 式 采 用 jacksontemplate.setValueSerializer(jackson2JsonRedisSerializer);// hash 的 value 序 列 化 方 式 采 用 jacksontemplate.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet();return template;}}

StringRedisTemplate:

在使用String類型存儲自定義對象時:

存入到Redis的數(shù)據(jù)會存儲一個該類對象的位置:

比如:

"@class": "com.sky.test.User",

這種方法更麻煩一點,需要每次手動地序列化,反序列化。

緩存

緩存就是數(shù)據(jù)交換的緩沖區(qū)(稱作Cache),是存貯數(shù)據(jù)的臨時地方,一般讀寫性能較高。

緩存:緩存穿透,緩存擊穿,緩存雪崩

雙寫一致性,緩存的持久化

數(shù)據(jù)過期策略,數(shù)據(jù)淘汰策略

緩存穿透

緩存穿透:查詢一個不存在的數(shù)據(jù)(在緩存中和數(shù)據(jù)庫中都不存在),mysql查詢不到數(shù)據(jù)也不會直接寫入緩存,就會導(dǎo)致每次請求都查數(shù)據(jù)庫

危害:如果有人惡意地讓很多線程并發(fā)地請求訪問這些不存在的數(shù)據(jù),那么這些所有的請求都會到達數(shù)據(jù)庫。而數(shù)據(jù)庫的并發(fā)不會很高,請求到達一定的量則會把數(shù)據(jù)庫搞垮,導(dǎo)致數(shù)據(jù)庫宕機。

解決方案一:緩存空數(shù)據(jù)

緩存空數(shù)據(jù),查詢返回的數(shù)據(jù)為空,仍把這個空結(jié)果進行緩存 。即{key:1,value:null}
優(yōu)點:簡單
缺點:消耗內(nèi)存,可能會發(fā)生不一致的問題

解決方案二:布隆過濾器

優(yōu)點:內(nèi)存占用較少,沒有多余key
缺點:實現(xiàn)復(fù)雜存在誤判

首先是緩存預(yù)熱時往布隆過濾器中添加數(shù)據(jù)(存儲數(shù)據(jù)過程)。

布隆過濾器主要是用于檢索一個元素是否在一個集合中。

我們當(dāng)時使用的是redisson實現(xiàn)的布隆過濾器。


它的底層主要是先去初始化一個比較大數(shù)組(bitmap),里面存放的二進制0或1。在一開始都是0,當(dāng)一個key來了之后經(jīng)過3次hash計算,模于數(shù)組長度找到數(shù)據(jù)的下標(biāo)然后把數(shù)組中原來的0改為1,這樣的話,三個數(shù)組的位置就能標(biāo)明一個key的存在。

查找的過程也是一樣的。

缺點:
布隆過濾器有可能會產(chǎn)生一定的誤判,我們一般可以設(shè)置這個誤判率,大概不會超過5%,其實這個誤判是必然存在的,要不就得增加數(shù)組的長度,其實已經(jīng)算是很劃算了,5%以內(nèi)的誤判率一般的項目也能接受,不至于高并發(fā)下壓倒數(shù)據(jù)庫。

誤判示例如下圖:

誤判率:數(shù)組越小誤判率就越大,數(shù)組越大誤判率就越小,但是同時帶來了更多的內(nèi)存消耗。

緩存擊穿

緩存擊穿的意思是對于設(shè)置了過期時間的key,緩存在某個時間點過期的時候,恰好這時間點對這個key有大量的并發(fā)請求過來,這些請求發(fā)現(xiàn)緩存過期,一般都會從后端DB加載數(shù)據(jù)并回設(shè)到緩存,這個時候大并發(fā)的請求可能會瞬間把DB壓垮。

解釋:對于Redis中正好過期的數(shù)據(jù)(Redis不存在數(shù)據(jù)了),此時如果有請求來訪問這些數(shù)據(jù),正常來說是會去查DB,同時DB把數(shù)據(jù)更新到Redis,再把數(shù)據(jù)返回。那么Redis也就得到了刷新,后續(xù)redis也可以繼續(xù)為DB分擔(dān)壓力。

但是把DB數(shù)據(jù)更新到Redis的過程中,可能會花費過多的時間(可能是因為DB刷新到redis的數(shù)據(jù)是多表的,多表統(tǒng)計費時間),在這個時間段內(nèi)redis的數(shù)據(jù)未重建完成,大量的并發(fā)請求過來的話則會全部走DB,會瞬間把DB壓垮。

如圖所示:

解決方案一:互斥鎖(分布式鎖)

使用互斥鎖:當(dāng)緩存過期失效時,不立即去load db,先使用如redis的setnx去設(shè)置一個互斥鎖,當(dāng)操作成功返回時再進行l(wèi)oad db的操作并回設(shè)緩存,否則重試get緩存的方法。

如圖所示:

互斥鎖保證了同時只能有一個線程獲得鎖去查詢數(shù)據(jù)庫并重建redis緩存數(shù)據(jù)。保證了數(shù)據(jù)的強一致性。

缺點:性能較低。

解決方案二:邏輯過期

Redis中的熱點數(shù)據(jù)的key不設(shè)置過期時間設(shè)置邏輯過期字段。
1、在設(shè)置key的時候,設(shè)置一個過期時間字段一塊存入緩存中,不給當(dāng)前key設(shè)置過期時間
2、當(dāng)查詢的時候,從redis取出數(shù)據(jù)后判斷時間是否過期
3、如果過期則開通另外一個線程進行數(shù)據(jù)同步,當(dāng)前線程正常返回數(shù)據(jù),這個數(shù)據(jù)不是最新

如:

key:1 value:{"id":"123", "title":"張三", "expire":153213455}

這種方案也是在查詢DB和重置邏輯過期時間時加上互斥鎖,其它線程來查詢緩存時要不就是得到還未更新的過期數(shù)據(jù),要不就得到更新后的數(shù)據(jù)。保證了在多個線程并發(fā)訪問時不把其它線程全部攔截住(就是不會讓它們一遍又一遍地重試獲取數(shù)據(jù))。

相比方案一更為高可用、性能優(yōu)。但由于可能會得到邏輯過期數(shù)據(jù),導(dǎo)致數(shù)據(jù)并不是絕對一致的。

特點:邏輯過期,更注重用戶體驗,高可用,性能優(yōu)。但不能保證數(shù)據(jù)絕對一致。

緩存雪崩

緩存雪崩是指設(shè)置緩存時采用了相同的過期時間,導(dǎo)致緩存在某一時刻同時失效,請求全部轉(zhuǎn)發(fā)到DB,DB瞬時壓力過重雪崩?;蛘呤?strong>Redis服務(wù)宏機,導(dǎo)致大量請求到達數(shù)據(jù)庫,帶來巨大壓力。

緩存雪崩與緩存擊穿的區(qū)別:
雪崩是很多key,擊穿是某一個key緩存。


第一種情況的解決方案是將緩存失效時間分散開,比如可以在原有的失效時間(TTL)基礎(chǔ)上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復(fù)率就會降低,就很難引發(fā)集體失效的事件。

其余解決方案:
1.利用Redis集群提高服務(wù)的可用性(哨兵模式,集群模式)
2.給緩存業(yè)務(wù)添加降級限流策略(ngxin或spring cloud gateway) (降級限流策略可做為系統(tǒng)的保底策略,適用于穿透,擊穿,雪崩)
3.給業(yè)務(wù)添加多級緩存 (Guava或Caffeine)

持久化

Redis是內(nèi)存數(shù)據(jù)庫,如果不將內(nèi)存中的數(shù)據(jù)庫狀態(tài)保存到磁盤,那么一旦服務(wù)器進程退出,服務(wù)器中的數(shù)據(jù)庫狀態(tài)也會消失(斷電即失),所以redis提供了持久化功能。

1.RDB

RDB執(zhí)行原理

這里存在一個問題:就是如果子進程共享的內(nèi)存數(shù)據(jù)時,主進程正在對共享的內(nèi)存數(shù)據(jù)進行更改。那么子進程就可能會得到一些臟數(shù)據(jù)。

解決方法:

主進程執(zhí)行寫操作時,共享數(shù)據(jù)會改成只讀數(shù)據(jù)。且會拷貝一份數(shù)據(jù)去執(zhí)行主進程的寫操作。

2.AOF

AOF全稱為Append Only File(追加文件)Redis處理的每一個寫命令都會記錄在AOF文件,可以看做是命令日志文件。

bgrewriteaof命令

因為是記錄命令,AOF文件會比RDB文件大的多。而且aof會記錄對同一個key的多次寫操作,但只有最后一次寫操作才有意義。通過執(zhí)行bgrewriteaof命令,可以讓AOF文件執(zhí)行重寫功能用最少的命令達到相同效果。

RDB與AOF對比

RDB和AOF各有自己的優(yōu)缺點,如果對數(shù)據(jù)安全性要求較高,在實際開發(fā)中往往會結(jié)合兩者來使用。

雙寫一致性

redis做為緩存,mysql的數(shù)據(jù)如何與redis進行同步呢? (雙寫一致性)

這個雙寫一致性最好要根據(jù)項目實際業(yè)務(wù)背景來說,一般分為兩種情況:
1.一致性要求高
2.允許延遲一致

1.一致性要求高

1.延遲雙刪

讀操作:緩存命中,直接返回;緩存未命中查詢數(shù)據(jù)庫,寫入緩存,設(shè)定超時時間
寫操作:延遲雙刪

1.無論第一步是先刪除緩存還是先修改數(shù)據(jù)庫都會導(dǎo)致臟數(shù)據(jù)的出現(xiàn)

所以重點是進行二次刪除緩存以及第二次刪除時做到延時刪除

2.刪除兩次緩存的原因

如果在第一次刪除緩存修改數(shù)據(jù)庫之間的時間里,另一個線程此時來查詢緩存了(未命中,查詢數(shù)據(jù)庫),那么此時寫入緩存的則是未更新的數(shù)據(jù)庫的數(shù)據(jù),為臟數(shù)據(jù)。如下圖所示:

所以在更新完數(shù)據(jù)庫后再次刪除緩存可以將這種情況下的臟數(shù)據(jù)盡量消除。

所以對緩存進行兩次刪除可以降低臟數(shù)據(jù)的出現(xiàn),但是不能杜絕。

3.延時刪除的原因

因為數(shù)據(jù)庫一般是主從模式的,讀寫分離了,主庫的數(shù)據(jù)同步到從庫需要一定的時間,故先要延遲一會。

問題:因為延遲的時間不好控制,所以還是可能會出現(xiàn)臟數(shù)據(jù)。

總結(jié)

延遲雙刪極大地控制了臟數(shù)據(jù)的風(fēng)險,但不可杜絕臟數(shù)據(jù)的風(fēng)險。

2.加讀寫鎖

能保證強一致性,但性能低

強一致性的,采用redisson提供的讀寫鎖
共享鎖:讀鎖readLock,加鎖之后,其他線程可以共享讀操作
排他鎖:獨占寫鎖writeLock,加鎖之后,阻塞其他線程讀寫操作

2.允許延遲一致(較為主流)

能保證最終一致性,會有短暫延遲。

1.異步通知保證數(shù)據(jù)的最終一致性

2.基于Canal的異步通知

數(shù)據(jù)過期策略

Redis對有些數(shù)據(jù)設(shè)置有效時間,數(shù)據(jù)過期以后,就需要將數(shù)據(jù)從內(nèi)存中刪除掉。可以按照不同的規(guī)則進行刪除,這種刪除規(guī)則就被稱之為數(shù)據(jù)的刪除策略(數(shù)據(jù)過期策略)。

方案一惰性刪除

惰性刪除,在設(shè)置該key過期時間后,我們不去管它,當(dāng)需要該key時,我們在檢查其是否過期,如果過期,我們就刪掉它,反之返回該key。


優(yōu)點:對CPU友好,只會在使用該key時才會進行過期檢查,對于很多用不到的key不用浪費時間進行過期檢查
缺點:對內(nèi)存不友好,如果一個key已經(jīng)過期。但是一直沒有使用,那么該key就會一直存在內(nèi)存中,內(nèi)存永遠不會釋放

方案二定期刪除:

定期刪除,就是說每隔一段時間,我們就對一些key進行檢查,刪除里面過期的key。
?

定期清理的兩種模式
1.SLOW模式是定時任務(wù),執(zhí)行頻率默認為10hz,每次不超過25ms,以通過修改配置文件redis.conf的hz選項來調(diào)整這個次數(shù)
2.FAST模式執(zhí)行頻率不固定,每次事件循環(huán)會嘗試執(zhí)行,但兩次間隔不低于2ms,每次耗時不超過1ms


優(yōu)點:可以通過限制刪除操作執(zhí)行的時長和頻率來減少刪除操作對CPU的影響。另外定期劇除,也能有效釋放過期鍵占用的內(nèi)存。
缺點:難以確定刪除操作執(zhí)行的時長和頻率。

定期清理控制時長和頻率--->盡量少占用主進程的操作--->減少對CPU的影響

總結(jié)

Redis的過期刪除策略:情性刪除+定期刪除兩種策略進行配合使用。

數(shù)據(jù)淘汰策略

數(shù)據(jù)的淘汰策略:當(dāng)Redis中的內(nèi)存不夠用時,此時在向Redis中添加新的key,那么Redis就會按照某一種規(guī)則將內(nèi)存中的數(shù)據(jù)刪除掉,這種數(shù)據(jù)的刪除規(guī)則被稱之為內(nèi)存的淘汰策略。

Redis支持8種不同策略來選擇要刪除的key:

八種不同策略

noeviction:不淘汰任何key,但是內(nèi)存滿時不允許寫入新數(shù)據(jù),默認就是這種策略。
volatile-ttl: 對設(shè)置了TTL的key,比較key的剩余TTL值,TTL越小越先被淘汰。 allkeys-random:對全體key,隨機進行淘汰
volatile-random:對設(shè)置了TTL的key,隨機進行淘汰
allkeys-lru:對全體key,基于LRU算法進行淘汰
volatile-lru:對設(shè)置了TTL的key,基于LRU算法進行淘汰
allkeys-lfu:對全體key,基于LFU算法進行淘汰
volatile-lfu:對設(shè)置了TTL的key,基于LFU算法進行淘汰


LRU(Least Recently Used)算法:最近最少使用。用當(dāng)前時間減去最
后一次訪問時間,這個值越大則淘汰優(yōu)先級越高。

例:key1是在6s之前訪問的,key2是在9s之前訪問的,刪除的就是key2


LFU(Least Frequently Used)算法:最少頻率使用。會統(tǒng)計每個key的
訪問頻率,值越小淘汰優(yōu)先級越高。

例:key1最近5s訪問了6次,key2最近5s訪問了9次,刪除的就是key1

數(shù)據(jù)淘汰策略--使用建議

1.優(yōu)先使用alkeys-lru策略。充分利用LRU算法的優(yōu)勢,把最近最常訪問的數(shù)據(jù)留在緩存中。如果業(yè)務(wù)有明顯的冷熱數(shù)據(jù)區(qū)分,建議使用這種策略。
2.如果業(yè)務(wù)中數(shù)據(jù)訪問頻率差別不大,沒有明顯冷熱數(shù)據(jù)區(qū)分,建議使用allkeys-random,隨機選擇淘汰。
3.如果業(yè)務(wù)中有置頂?shù)男枨?#xff0c;可以使用volatile-lru策略,同時置頂數(shù)據(jù)不設(shè)置過期時間,這些數(shù)據(jù)就一直不被刪除
會淘汰其他設(shè)置過期時間的數(shù)據(jù)。
4.如果業(yè)務(wù)中有短時高頻訪問的數(shù)據(jù),可以使用allkeys-lfu 或volatile-lfu 策略。

常見問題:
1.數(shù)據(jù)庫有1000萬數(shù)據(jù),Redis只能緩存2ow數(shù)據(jù),如何保證Redis中的數(shù)據(jù)都是熱點數(shù)據(jù)?
使用allkeys-lru(挑選最近最少使用的數(shù)據(jù)淘汰)淘汰策略,留下來的都是經(jīng)常訪問的熱點數(shù)據(jù)
2.Redis的內(nèi)存用完了會發(fā)生什么?
默認的配置(noeviction):會直接報錯

分布式鎖

場景

通常情況下,分布式鎖使用的場景:
集群情況下的定時任務(wù),搶單搶券,秒殺,冪等性場景

引入與基本介紹

如果項目是單體項目,只啟動了一臺服務(wù),那遇到這類搶單問題時(防止超賣),可以加synchronized鎖解決。(解決多線程并發(fā)環(huán)境下的問題)

但是項目服務(wù)是集群部署的話,那么synchronized鎖這種本地鎖(只能保證單個JVM內(nèi)部的多個線程之間互斥,不能讓集群下的多個JVM下的多個線程互斥)(只對本服務(wù)器有效)會失效,需要使用外部鎖,也就是分布式鎖。

例1(搶券場景):

例2:

分布式鎖:滿足分布式系統(tǒng)或集群模式下多進程可見并且互斥的鎖。

實現(xiàn)分布式鎖的方式有很多,常見的有三種:

redis分布式鎖實現(xiàn)原理

基本介紹

設(shè)置超時失效時間的原因(避免死鎖):

如果某個線程拿到鎖在執(zhí)行業(yè)務(wù)時,服務(wù)器突然宕機,此時這個線程還沒來得及釋放鎖,而如果沒有設(shè)置過期時間的話,這個鎖就沒辦法得到釋放了,別的線程怎么也獲取不到這個鎖了,就造成了死鎖。而設(shè)置了過期時間的話,鎖到時間了就會自動釋放。

總結(jié)

缺陷

以上的問題是比較小的可能出現(xiàn)的,但是我們用Redis實現(xiàn)的分布式鎖去解決又顯得尤為困難,所以我們可以去使用Redisson框架,它底層提供了以上問題的解決方案,方便了我們?nèi)ソ鉀Q問題。

Redisson實現(xiàn)的分布式鎖

redisson是Redis的一個框架。

Redisson是一個在Redis的基礎(chǔ)上實現(xiàn)的Java駐內(nèi)存數(shù)據(jù)網(wǎng)格。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務(wù),其中就包含了各種分布式鎖的實現(xiàn)。

快速入門:


在Redisson中需要手動加鎖,并且可以控制鎖的失效時間和等待時間,當(dāng)鎖住的一個業(yè)務(wù)還沒有執(zhí)行完成的時候,在redisson中引入了一個看門狗機制,就是說每隔一段時間就檢查當(dāng)前業(yè)務(wù)是否還持有鎖,如果持有就增加鎖的持有時間,當(dāng)業(yè)務(wù)執(zhí)行完成之后需要釋放鎖。
在高并發(fā)下,一個業(yè)務(wù)有可能會執(zhí)行很快,先客戶1持有鎖的時候,客戶2來了以后并不會馬上拒絕,它會自旋不斷嘗試獲取鎖(while循環(huán)獲取),如果客戶1釋放之后,客戶2就可以馬上持有鎖,性能也得到了提升。

1.redisson實現(xiàn)的分布式鎖的執(zhí)行流程/合理地控制鎖的有效時長(失效時間)

原因:如果鎖的有效時間設(shè)置的不合理,可能業(yè)務(wù)還沒執(zhí)行完鎖就釋放了,那此時其它線程來也可以獲取到鎖,就破壞了業(yè)務(wù)執(zhí)行的原子性,業(yè)務(wù)數(shù)據(jù)會受到影響。


方法:根據(jù)業(yè)務(wù)所需時間實時給鎖續(xù)期。

//可重試:利用信號量PubSub功能實現(xiàn)等待,喚醒,獲取鎖失敗的重試機制。

releaseTime默認是30s。

另外開一個線程“看門狗”來監(jiān)視持有鎖的線程并做續(xù)期任務(wù)(每隔releaseTime/3的時間做一次續(xù)期)。

public void redislock() thr throws interruptedexception {//獲取鎖(重入鎖),執(zhí)行鎖的名稱RLock lock = redissonClient.getLock("lock");//嘗試獲取鎖,//參數(shù)分別是:獲取鎖的最大等待時間(期間會重試),鎖自動釋放時間(鎖失效時間),時間單位//boolean islock  = lock.tryLock(10,30,TimeUnit.SECONDS);boolean isLock = lock.tryLock(10,TimeUnit.SECONDS);//參數(shù):1.鎖的最大等待時間:鎖通過while循環(huán)來不斷嘗試獲取鎖的最大等待時間,如果這個時間內(nèi)沒有獲取到鎖則放棄獲取鎖。//      2.鎖自動釋放時間:最好不要設(shè)置或者設(shè)置為-1,否則不會啟動看門狗線程進行續(xù)期任務(wù)。//		3.時間單位//加鎖,釋放鎖,設(shè)置過期時間,給鎖續(xù)期等操作都是基于lua腳本完成。//Lua腳本可以調(diào)用Redis命令來保證多條命令執(zhí)行的原子性。//判斷是否獲取成功             if(isLock){try{System.out.println("執(zhí)行業(yè)務(wù)");} finally {//釋放鎖lock.unlock();}}
}

原子性問題:

Redis提供了Lua腳本功能,在一個腳本中編寫多條Redis命令,確保多條命令執(zhí)行時的原子性。

2.可重入

Redis實現(xiàn)的鎖是不可重入的,但redisson實現(xiàn)的鎖是可重入的。

作用:避免死鎖的產(chǎn)生。

這個重入其實在內(nèi)部就是判斷是否是當(dāng)前線程持有的鎖。如果是當(dāng)前線程持有的鎖就會計數(shù),如果釋放鎖就會在計算上減一。

存儲數(shù)據(jù)的時候采用的hash結(jié)構(gòu),大key可以按照自己的業(yè)務(wù)進行定制,其中小key是當(dāng)前線程的唯一標(biāo)識線程id),value是當(dāng)前線程重入的次數(shù)。

public void add1(){RLock lock = redissonClient.getLock("heimalock");boolean islock = lock.tryLock();//執(zhí)行業(yè)務(wù)add2();//釋放鎖lock.unlock();
}
public void add2(){RLock lock = redissonClient.getLock("heimalock");boolean islock = lock.trylock();//執(zhí)行業(yè)務(wù)...//釋放鎖lock.unlock();
}

底層獲取鎖釋放鎖等操作都很復(fù)雜,都是有多個步驟,所以是用Lua腳本寫確保各個操作的原子性。

3.主從一致性

redisson實現(xiàn)的分布式鎖不能解決主從一致性問題。

比如,當(dāng)線程1加鎖成功后,Master節(jié)點數(shù)據(jù)會異步復(fù)制到Slave節(jié)點,當(dāng)數(shù)據(jù)還沒來得及同步到Slave節(jié)點時,當(dāng)前持有Redis鎖的Master節(jié)點宕機,Slave節(jié)點被提升為新的Master節(jié)點。(按道理主節(jié)點和從節(jié)點的數(shù)據(jù)應(yīng)該要是一模一樣的,加鎖的信息也要一模一樣(其實就是一個setnx數(shù)據(jù)而已))

假如現(xiàn)在來了一個線程2,再次加鎖,因為Master節(jié)點數(shù)據(jù)還沒來得及同步過來(從節(jié)點已經(jīng)被這把鎖鎖住且線程一已經(jīng)拿到了這把鎖的信息還未更新過來),所以會在新的Master節(jié)點上加鎖成功,這個時候就會出現(xiàn)兩個線程同時持有一把鎖的問題。

兩個線程同時獲取一把鎖--->違背了鎖的互斥性(鎖失效了)。

紅鎖:

紅鎖算法的基本思想是,當(dāng)需要鎖定多個資源時,可以在多個Redis節(jié)點上分別獲取鎖,只有當(dāng)大多數(shù)節(jié)點上
的鎖都被成功獲取時,整個鎖才算獲取成功。這樣可以提高系統(tǒng)的容錯性和可用性。


我們可以利用Redisson提供的紅鎖來解決這個問題,它的主要作用是,不能只在一個redis實例上創(chuàng)建鎖,應(yīng)該是在多個redis實例上創(chuàng)建鎖,并且要求在大多數(shù)Redis節(jié)點上都成功創(chuàng)建鎖,紅鎖中要求是Redis的節(jié)點數(shù)量要過半。這樣就能避免線程1加鎖成功后Master節(jié)點宕機導(dǎo)致線程2成功加鎖到新的Master節(jié)點上的問題了。

意思就是線程來的時候要獲取多個Redis節(jié)點的鎖才算成功,才可以執(zhí)行代碼。

如果一個主節(jié)點宕機(主節(jié)點的數(shù)據(jù)還沒來得及同步到從節(jié)點,與以上同理),它的從節(jié)點變成主節(jié)點,那么此時另一個線程來是不可以獲取到鎖的,因為這個線程必須要獲取到所有的節(jié)點的鎖才能成功獲取到鎖,它只能拿到宕機的那個主節(jié)點的從節(jié)點的鎖(因為主節(jié)點的數(shù)據(jù)還沒來得及同步到從節(jié)點),所以會獲取鎖失敗。

只要有一個節(jié)點是存活的,其它線程就不可以拿到鎖,鎖就不會失效。


缺點

如果使用了紅鎖,因為需要同時在多個節(jié)點上都添加鎖,性能就變的很低了,并且運維維護成本也非常高,所以,我們一般在項目中也不會直接使用紅鎖,并且官方也暫時廢棄了這個紅鎖。
所以強一致性要求高的業(yè)務(wù),建議使用zookeeper實現(xiàn)的分布式鎖,它是可以保證強一致性的。

Redis發(fā)布訂閱

  • Redis 發(fā)布訂閱(pub/sub)是一種消息通信模式: 發(fā)送者(pub)發(fā)送消息,訂閱者(sub)接受消息.微博,微信,關(guān)注系統(tǒng)
  • Redis 客戶端可以訂閱任意數(shù)量的頻道。

訂閱/發(fā)布消息圖

  • 第一個: 消息發(fā)送者, 第二個 :頻道 第三個 :消息訂閱者!

  • 下圖展示了頻道 channel1 , 以及訂閱這個頻道的三個客戶端 —— client2 、 client5 和 client1 之間的關(guān)系:

  • 當(dāng)有新消息通過 PUBLISH 命令發(fā)送給頻道 channel1 時, 這個消息就會被發(fā)送給訂閱它的三個客戶端:

命令

  • 這些命令被廣泛用于構(gòu)建即時通信應(yīng)用,比如網(wǎng)絡(luò)聊天室和實時廣播,實時提醒

測試

  • 訂閱端(消費者)
    • 開啟客戶端1
127.0.0.1:6379> subscribe codeyuaiiao //訂閱一個頻道,頻道名稱:codeyuaiiao  訂閱的時候頻道就建立了
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "codeyuaiiao"
3) (integer) 1# 等待讀取推送(客戶端2發(fā)送消息,客戶端1這邊接收消息)
1) "message" # 消息
2) "codeyuaiiao" # 消息來自哪個頻道
3) "hello world" # 消息的具體內(nèi)容1) "message"
2) "codeyuaiiao"
3) "hello yuhaijiao"
  • 發(fā)送端:(生產(chǎn)者)
    • 再開啟一個客戶端2
127.0.0.1:6379> publish codeyuaiiao "hello world" # 發(fā)布者發(fā)布消息到頻道
(integer) 1
127.0.0.1:6379> publish codeyuaiiao "hello yuhaijiao" # 發(fā)布者發(fā)布消息到頻道
(integer) 1
127.0.0.1:6379> 

Redis發(fā)布訂閱原理

  • Redis是使用C實現(xiàn)的,通過分析 Redis 源碼里的 pubsub.c 文件,了解發(fā)布和訂閱機制的底層實現(xiàn),籍此加深對 Redis 的理解。
  • Redis 通過 PUBLISH(發(fā)送消息) 、SUBSCRIBE(訂閱頻道) 和 PSUBSCRIBE(訂閱多個頻道) 等命令實現(xiàn)發(fā)布和訂閱功能。

例如微信訂閱公眾號:

  • 通過 SUBSCRIBE 命令訂閱某頻道后
    • Redis-server 里維護了一個字典
    • 字典的鍵就是一個個頻道
    • 字典的值則是一個鏈表,鏈表中保存了所有訂閱這個 channel 的客戶端
    • SUBSCRIBE 命令的關(guān)鍵, 就是將客戶端添加到給定 channel 的訂閱鏈表中
  • 通過publish命令向訂閱者發(fā)送消息,redis-server會使用給定的頻道作為鍵,在它所維護的channel字典中查找記錄了訂閱這個頻道的所有客戶端的鏈表,遍歷這個鏈表,將消息發(fā)布給所有訂閱者.

總結(jié)

Pub/Sub 從字面上理解就是發(fā)布(Publish)與訂閱(Subscribe),在Redis中,你可以設(shè)定對某一個key值進行消息發(fā)布及消息訂閱,當(dāng)一個key值上進行了消息發(fā)布后,所有訂閱它的客戶端都會收到相應(yīng)的消息。這一功能最明顯的用法就是用作實時消息系統(tǒng),比如普通的即時聊天,群聊等功能。

使用場景

  1. 實時消息系統(tǒng)
  2. 實時聊天! (頻道當(dāng)做聊天室,將信息回顯給所有人即可! )
  3. 訂閱,關(guān)注系統(tǒng)都是可以的

稍微復(fù)雜的場景我們就會使用 消息中間件MQ

Redis消息隊列

概念

消息隊列(Message Queue),字面意思就是存放消息的隊列。最簡單的消息隊列模型包括3個角色:
消息隊列:存儲和管理消息,也被稱為消息代理(message broker)
生產(chǎn)者:發(fā)送消息到消息隊列
消費者:從消息隊列獲取消息并處理消息

例(秒殺搶券業(yè)務(wù)):

生產(chǎn)者:判斷是否有資格搶券(券的剩余數(shù)量大于0且當(dāng)前用戶之前未搶到券),如果有資格則將訂單相關(guān)信息寫入消息隊列

消費者:開啟一個獨立的線程去接收消息,完成下單(把訂單信息寫入Mysql數(shù)據(jù)庫)

這樣秒殺搶單的業(yè)務(wù)真正寫數(shù)據(jù)庫的業(yè)務(wù)就實現(xiàn)了分離,變成了異步操作,解耦合了。

秒殺搶單的業(yè)務(wù):秒殺這里因為不用寫數(shù)據(jù)庫(比較耗時),并發(fā)能力大大提高。
寫數(shù)據(jù)庫的業(yè)務(wù):可以根據(jù)自己的節(jié)奏慢慢地去取訂單寫數(shù)據(jù)庫,不會讓數(shù)據(jù)庫有太大的壓力,保證數(shù)據(jù)庫抗得住。

Redis提供了三種不同的方式來實現(xiàn)消息隊列:
list結(jié)構(gòu):基于List結(jié)構(gòu)模擬消息隊列
PubSub:基本的點對點消息模型
Stream:比較完善的消息隊列模型

基于List結(jié)構(gòu)模擬消息隊列(可實現(xiàn)阻塞隊列的效果)

支持持久化:因為list類型redis本身是用鏈表做存儲數(shù)據(jù)的,只是我們把它當(dāng)成消息隊列來用,故對數(shù)據(jù)可以持久化

基于PubSub(發(fā)布訂閱)的消息隊列

主要內(nèi)容就是上面學(xué)習(xí)的Redis發(fā)布訂閱


優(yōu)點:
采用發(fā)布訂閱模型,支持多生產(chǎn),多消費
缺點:
不支持?jǐn)?shù)據(jù)持久化
無法避免消息丟失
消息堆積有上限,超出時數(shù)據(jù)丟失

不支持持久化:因為PubSub本身就只是用來做發(fā)布訂閱功能的,如果沒有人訂閱某個頻道,那么往這個頻道發(fā)布數(shù)據(jù)后,數(shù)據(jù)會丟失,Redis不會保存這個數(shù)據(jù)。

基于Stream的消息隊列

基本知識

Stream是Redis 5.0引入的一種新數(shù)據(jù)類型,可以實現(xiàn)一個功能非常完善的消息隊列。

例:

注意
當(dāng)我們指定起始id為$時,代表讀取最新的消息,如果我們處理一條消息的過程中,又有超過一條以上的消息到達隊列,則下次獲取時也只能獲取到最新的一條,會出現(xiàn)漏讀消息的問題。

Stream類型消息隊列的XREAD命令特點:
1.消息可回溯
2.一個消息可以被多個消費者讀取
3.可以阻塞讀取
4.有消息漏讀的風(fēng)險

消費者組

消費者組:將多個消費者劃分到一個組中,監(jiān)聽同一個隊列。

具備下列特點:

Stream類型消息隊列的XREADGROUP命令特點:
消息可回溯
可以多消費者爭搶消息,加快消費速度
可以阻塞讀取
沒有消息漏讀的風(fēng)險
有消息確認機制,保證消息至少被消費一次

Redis集群(分布式緩存)

單點Redis的問題

1.并發(fā)能力問題

解決方法:搭建主從集群,實現(xiàn)讀寫分離。實現(xiàn)高并發(fā)。

2.故障恢復(fù)問題

解決方法:利用Redis哨兵,實現(xiàn)健康檢測和自動恢復(fù)。保障高可用。

3.存儲能力問題

解決方法:搭建分片集群,利用插槽機制實現(xiàn)動態(tài)擴容。

主從復(fù)制

單節(jié)點Redis的并發(fā)能力是有上限的,要進一步提高Redis的并發(fā)能力,就需要搭建主從集群,實現(xiàn)讀寫分離。

主從數(shù)據(jù)同步原理
1.主從全量同步

注:

1.判斷是否第一次同步:

從節(jié)點的replid與主節(jié)點的不一樣則說明這個從節(jié)點是第一次同步。

2.只有第一次同步的時候主節(jié)點才會生成RDB文件,第一次之后的同步會根據(jù)偏移量利用repl_baklog日志文件進行同步數(shù)據(jù)。

2.主從增量同步(slave重啟或后期數(shù)據(jù)變化)

3.總結(jié)

全量同步:
1.從節(jié)點請求主節(jié)點同步數(shù)據(jù)(replication id, offset)
2.主節(jié)點判斷是否是第一次請求,是第一次就與從節(jié)點同步版本信息(replicationid和offset)
3.主節(jié)點執(zhí)行bgsave,生成rdb文件后,發(fā)送給從節(jié)點去執(zhí)行
4.在rdb生成執(zhí)行期間,主節(jié)點會以命令的方式記錄到緩沖區(qū)(一個日志文件)
5.把生成之后的命令日志文件發(fā)送給從節(jié)點進行同步

增量同步:
1.從節(jié)點請求主節(jié)點同步數(shù)據(jù),主節(jié)點判斷不是第一次請求,不是第一次就獲取從節(jié)點的offset值
2.主節(jié)點從命令日志中獲取offset值之后的數(shù)據(jù),發(fā)送給從節(jié)點進行數(shù)據(jù)同步

哨兵模式

redis提供了哨兵模式來實現(xiàn)主從集群的自動故障恢復(fù),從而極大地保障了Redis主從高可用。

哨兵模式的結(jié)構(gòu)與作用

redis提供了哨兵 (Sentinel)機制來實現(xiàn)主從集群的自動故障恢復(fù)。

結(jié)構(gòu):

作用:

監(jiān)控:Sentinel會不斷檢查您的master和slave是否按預(yù)期工作
自動故障恢復(fù):如果master故障, Sentinel會將一個slave提升為master。當(dāng)故障實例恢復(fù)后也以新的master為主
通知:Sentinel充當(dāng)redis客戶端的服務(wù)發(fā)現(xiàn)來源,當(dāng)集群發(fā)生故障轉(zhuǎn)移時,會將最新信息推送給redis的客戶端

服務(wù)狀態(tài)監(jiān)控

Sentinel基于心跳機制監(jiān)測服務(wù)狀態(tài),每隔1秒向集群的每個實例發(fā)送ping命令:
主觀下線:如果某sentinel節(jié)點發(fā)現(xiàn)某實例未在規(guī)定時間響應(yīng),則認為該實例主觀下線
客觀下線:若超過指定數(shù)量(quorum)的sentinel都認為該實例主觀下線,則該實例客觀下線。quorum值最好
超過sentinel實例數(shù)量的一半。

哨兵選主規(guī)則(主節(jié)點宕機后,選從節(jié)點為主節(jié)點的規(guī)則)

1.首先判斷主與從節(jié)點斷開時間長短,如超過指定值就排除該從節(jié)點
2.然后判斷從節(jié)點的slave-priority值,越小優(yōu)先級越高
3.如果slave-prority一樣,則判斷slave節(jié)點的offset值,越大優(yōu)先級越高.
4.最后是判斷slave節(jié)點的運行id大小,越小優(yōu)先級越高。

第三條最重要!!!

Redis集群(哨兵模式)的腦裂問題

有的時候由于網(wǎng)絡(luò)等原因可能會出現(xiàn)腦裂的情況,就是說,由于redis的master節(jié)點和redis的salve節(jié)點和sentinel處于不同的網(wǎng)絡(luò)分區(qū),使得sentinel沒有能夠心跳感知到主節(jié)點,所以通過選舉的方式提升了一個salve為master,這樣就存在了兩個master,就像大腦分裂了一樣,這樣會導(dǎo)致客戶端還在old master那里寫入數(shù)據(jù)新節(jié)點無法同步數(shù)據(jù),當(dāng)網(wǎng)絡(luò)恢復(fù)后,sentinel會將old master降為salve,這時再從新master同步數(shù)據(jù),就會導(dǎo)致old master中的大量數(shù)據(jù)丟失。

----------->

------------>

解決方法

在redis的配置中設(shè)置兩個配置參數(shù)

1.(min-replicas-to-write 1)設(shè)置最少的salve節(jié)點個數(shù)為1,設(shè)置至少要有一個從節(jié)點才能同步數(shù)據(jù)

2.(min-replicas-max-lag 5)設(shè)置主從數(shù)據(jù)復(fù)制和同步的延遲時間不能超過5秒

達不到要求就拒絕請求,就可以避免大量的數(shù)據(jù)丟失。

總結(jié):
我們可以修改redis的配置,可以設(shè)置最少的從節(jié)點數(shù)量至少為一個以及縮短主從數(shù)據(jù)同步的延遲時間(不能超過5秒),達不到要求就拒絕Redis客戶端的請求(不讓客戶端寫入數(shù)據(jù)到老的主節(jié)點),這樣就可以避免大量的數(shù)據(jù)丟失。

分片集群

主從和哨兵可以解決高可用,高并發(fā)讀的問題。但是依然有兩個問題沒有解決;
1.海量數(shù)據(jù)存儲問題
2.高并發(fā)寫的問題

使用分片集群可以解決上述問題,分片集群特征
1.集群中有多個master,每個master保存不同數(shù)據(jù)
2.每個master都可以有多個slave節(jié)點
3.master之間通過ping監(jiān)測彼此健康狀態(tài) (這點類似于之前的哨兵模式)
4.客戶端請求可以訪問集群任意節(jié)點,最終都會被轉(zhuǎn)發(fā)到正確節(jié)點 (路由:客戶端請求可以訪問集群任意節(jié)點,最終都會被轉(zhuǎn)發(fā)到正確節(jié)點。)

具體的路由規(guī)則:

Redis分片集群引入了哈希槽的概念,Redis集群有16384個哈希槽,每個key通過 CRC16 校驗后對 16384 取模來
決定放置哪個槽,集群的每個節(jié)點負責(zé)一部分hash槽。

Redis分片集群中數(shù)據(jù)的存儲和讀取:
redis分片集群引入了哈希槽的概念,redis集群有16384個哈希槽,將16384個插槽分配到不同的實例
讀寫數(shù)據(jù):根據(jù)key的有效部分計算哈希值。對16384取余(有效部分,如果key前面有大括號的
內(nèi)容就是有效部分,如果沒有,則以key本身做為有效部分)余數(shù)做為插槽,尋找插槽所在的實例

redis集群環(huán)境部署(環(huán)境配置)

只配置從庫,不用配置主庫!

  • 原因:redis默認都是主庫

查看當(dāng)前redis庫的信息,分析是否是主庫

  • 命令:info replication
127.0.0.1:6379> info replication	# 查看當(dāng)前庫的信息
# Replication
role:master	# 角色 master	
master connected_slaves:0 # 沒有從機
master_replid:b63c90e6c501143759cb0e7f450bd1eb0c70882a 
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0 
second_repl_offset:-1 
repl_backlog_active:0 
repl_backlog_size:1048576 
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

搭建redis集群準(zhǔn)備工作

  • 復(fù)制3個redis.conf配置文件,然后修改對應(yīng)的集群信息
  • 分別修改3個redis.conf對應(yīng)的以下4個屬性配置
  1. port端口修改
  1. pid名字
  1. log文件名字
  1. dump.rdb名字
  • 修改完畢之后,啟動我們的3個redis服務(wù)器
    • 分別啟動3個redis服務(wù)命令
  • 啟動完畢,通過進程查看信息

一主二從集群搭建(命令或文件配置)(這種方式的redis集群實際工作用不到,僅供基礎(chǔ)學(xué)習(xí))

命令方式配置

默認情況下, 每臺Redis 服務(wù)器都是主節(jié)點?; 我們一般情況下只用配置從機就好了!

  • 認老大! 一主 (6379)二從(6380,6381)
  • 配置從機,去6380和6381配置,命令:
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379	#	SLAVEOF host 6379	登錄6380找6379主機當(dāng)自己的老大!
OK
127.0.0.1:6380> info replication # 查詢信息redis6380的服務(wù)信息
role:slave	# 查看當(dāng)前角色是從機
master_host:127.0.0.1	# 可以的看到主機的信息和端口號
master_port:6379
  • 配置結(jié)果查詢:6380,6381的角色role變成了slave從機

  • 如果兩個都配置完了,就有兩個從機了
    • 登錄6379主機,查看主機下的兩個從節(jié)點

  • 真實的主從配置應(yīng)該在配置文件中配置,這樣的話是永久的, 我們這里使用的是命令,暫時的!
文件方式配置(一主二從,持久化的,對于哨兵模式,不建議使用這種)
  • 登錄redis從機,進入redis.conf配置文件配置replicaof
  • 如果主機有密碼,則配置主機密碼

一主二從細節(jié)

  • 主機可以寫, 從機不能寫只能讀! 主機中的所有信息和數(shù)據(jù),都會自動被從機保存.

測試主機寫,從機讀

  • 主機寫:

  • 從機讀:

  • 從機寫,會報錯

測試: 主機宕機斷開連接,從機會有什么變化

  • 從機沒有變化,依然指向主機,并且只能讀不能寫
  • 如果想把從機改為主機,只能手動去設(shè)置,或者配置哨兵通過選舉,將從機變?yōu)橹鳈C

測試2:這個時候, 主機如果回來了,從機有什么變化

  • 從機依舊可以直接獲取到主機寫的信息!保證高可用性

測試3:如果從機斷了,會有什么后果

  • 由于是使用命令行來配置的從機,這個時候如果從機重啟了,就會變成主機 (所以建議在redis.conf配置文件中配置從機)!
  • 但只要重新將主機變?yōu)閺臋C, 立馬就會從主機中獲取值!

主從復(fù)制原理

  • Slave啟動成功連接到master后會發(fā)送一個sync同步命令
  • Master接到命令,啟動后臺的存盤進程,同時收集所有接收到的用于修改數(shù)據(jù)集命令, 在后臺進程完畢之后,master將傳送整個數(shù)據(jù)文件到slave,并完成一次完全同步.
  • 全量復(fù)制: 而slave服務(wù)在接收到數(shù)據(jù)庫文件數(shù)據(jù)后, 將其存盤并加載到內(nèi)存中.
  • 增量復(fù)制: Master繼續(xù)將新的所有收集到的修改命令依次傳給slave,完成同步.
  • 但是只要是重新連接master, 一次完全同步(全量復(fù)制)將被自動執(zhí)行! 我們的數(shù)據(jù)一定可以在從機中看到!
一主兩從的第二種搭建方式(層層鏈路)哨兵模式的手動版

層層鏈路

  • 79是主節(jié)點
  • 80是79的從節(jié)點
  • 81是79的從節(jié)點
  • 上一個M連接下一個S!

  • 這時候也可以完成我們的主從復(fù)制!

如果沒有老大了,這個時候能不能選擇一個老大出來呢? 手動!

  • 謀朝篡位
    • 如果主機斷開了連接, 我們可以使用slaveof no one?讓自己變成主機! 其他的節(jié)點就可以手動連接到最新的這個主節(jié)點(手動)! 如果這個時候老大修復(fù)了, 那就只能重新配置連接!
    • 所以建議使用命令配置集群,方便將從節(jié)點改為主節(jié)點后,不用在去改配置文件

I/O多路復(fù)用模型

redis為什么這么快

用戶空間和內(nèi)核空間

Linux系統(tǒng)中一個進程使用的內(nèi)存情況劃分兩部分:內(nèi)核空間,用戶空間

用戶空間只能執(zhí)行受限的命令(Ring3),而且不能直接調(diào)用系統(tǒng)資源(比如網(wǎng)卡數(shù)據(jù)),必須通過內(nèi)核提供的接口來訪問。
內(nèi)核空間可以執(zhí)行特權(quán)命令(Ring0),調(diào)用一切系統(tǒng)資源。

Linux系統(tǒng)為了提高IO效率,會在用戶空間和內(nèi)核空間加入緩沖區(qū)
寫數(shù)據(jù)時,要把用戶緩沖數(shù)據(jù)拷貝到內(nèi)核緩沖區(qū),然后寫入設(shè)備
讀數(shù)據(jù)時,要從設(shè)備讀取數(shù)據(jù)到內(nèi)核緩沖區(qū),然后拷貝到用戶緩沖區(qū)

如圖所示

阻塞IO

顧名思義,阻塞IO就是兩個階段都必須阻塞等待
階段一:
1.用戶進程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
2.此時數(shù)據(jù)尚未到達,內(nèi)核需要等待數(shù)據(jù)
3.此時用戶進程也處于阻塞狀態(tài)

階段二:
1.數(shù)據(jù)到達并拷貝到內(nèi)核緩沖區(qū),代表已就緒
2.將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
3.拷貝過程中,用戶進程依然阻塞等待
4.拷貝完成,用戶進程解除阻塞,處理數(shù)據(jù)

可以看到,阻塞IO模型中,用戶進程在兩個階段都是阻塞狀態(tài)。

非阻塞IO

顧名思義,非阻塞IO的recvfrom操作立即返回結(jié)果而不是阻塞用戶進程。

階段一:
1.用戶進程嘗試讀取數(shù)據(jù)(比如網(wǎng)卡數(shù)據(jù))
2.此時數(shù)據(jù)尚未到達,內(nèi)核需要等待數(shù)據(jù)
3.返回異常給用戶進程
4.用戶進程拿到error后,再次嘗試讀取
5.循環(huán)往復(fù),直到數(shù)據(jù)就緒
階段二:
將內(nèi)核數(shù)據(jù)拷貝到用戶緩沖區(qū)
拷貝過程中,用戶進程依然阻塞等待
拷貝完成,用戶進程解除阻塞,處理數(shù)據(jù)

可以看到,非阻塞IO模型中,用戶進程在第一個階段是非阻露,第二個階段是阻塞狀態(tài)。雖然是非阻塞,但性能并沒有得到提高。而且忙等機制導(dǎo)致CPU空轉(zhuǎn),CPU使用率暴增。

IO多路復(fù)用

Redis網(wǎng)絡(luò)模型

Redis通過IO多路復(fù)用來提高網(wǎng)絡(luò)性能,并且支持各種不同的多路復(fù)用實現(xiàn),并且將這些實現(xiàn)進行封裝,提供了統(tǒng)一的高性能事件庫。

主要是IO多路復(fù)用+事件派發(fā)機制:

Redis 6.0之后,為了提升性能,引入了多線程處理:

http://aloenet.com.cn/news/32864.html

相關(guān)文章:

  • 網(wǎng)站建設(shè)夢幻創(chuàng)意百度文庫官網(wǎng)
  • php做的網(wǎng)站安全嗎今天的新聞頭條
  • 什么公司在百度做網(wǎng)站常州seo關(guān)鍵詞排名
  • 做網(wǎng)站實習(xí)日志寧波seo怎么做引流推廣
  • 陽泉購物網(wǎng)站開發(fā)設(shè)計市場營銷策劃
  • 網(wǎng)站網(wǎng)絡(luò)廣告如何建設(shè)自助建站免費搭建個人網(wǎng)站
  • 織夢網(wǎng)站后臺關(guān)鍵詞推廣優(yōu)化app
  • 婚禮顧問網(wǎng)站介紹模版有哪些營銷推廣方式
  • 用php做動態(tài)網(wǎng)站嗎企業(yè)中層管理人員培訓(xùn)課程
  • 手機網(wǎng)站內(nèi)容模塊如何進行網(wǎng)站宣傳推廣
  • 58網(wǎng)站怎么做優(yōu)化迅雷磁力鏈bt磁力種子
  • 合肥疫情風(fēng)險等級思億歐seo靠譜嗎
  • 天津企業(yè)網(wǎng)站建站做一個公司網(wǎng)站大概要多少錢
  • 用自己電腦做網(wǎng)站的空間百度自媒體注冊入口
  • 南寧seo費用服務(wù)短視頻seo系統(tǒng)
  • 黨政機關(guān)如何建設(shè)網(wǎng)站企業(yè)推廣網(wǎng)絡(luò)營銷外包服務(wù)
  • 做網(wǎng)站如何與美工配合日本比分預(yù)測
  • 網(wǎng)站開發(fā)速成班網(wǎng)絡(luò)軟文怎么寫
  • 天津建設(shè)工程信息網(wǎng) 官網(wǎng)首頁seo排名點擊器曝光行者seo
  • 鄂州做網(wǎng)站公司推廣營銷軟件app
  • 建設(shè)銀行梅州分行網(wǎng)站廈門seo怎么做
  • 武漢 網(wǎng)站制作案例北京建站
  • 平頂山網(wǎng)站建設(shè)費用競價排名是什么
  • 網(wǎng)站建設(shè)流程有東莞網(wǎng)站推廣方案
  • 成都模板網(wǎng)站建設(shè)網(wǎng)絡(luò)推廣優(yōu)化平臺
  • 下沙開發(fā)區(qū)建設(shè)局網(wǎng)站廣州軟文推廣公司
  • 威海做網(wǎng)站多少錢百度助手app下載
  • 網(wǎng)站建設(shè)計劃網(wǎng)絡(luò)營銷有什么崗位
  • 做網(wǎng)站論文網(wǎng)絡(luò)營銷的模式有哪些?
  • 做二手車網(wǎng)站怎么做的外貿(mào)軟件排行榜