最近做了個關於redis的項目,那么就整理下遇到和未遇到的問題
1、redis的簡介安裝
2、redis的數據結構
3、Redis基本使用
4、Redis的並發
5、Redis的落地
一、redis的簡介安裝
一、Redis 是什么
Redis 是一款依據BSD開源協議發行的高性能Key-Value存儲系統(cache and store)。它通常被稱為數據結構服務器,因為值(value)可以是 字符串(String), 哈希(Map), 列表(list), 集合(sets) , 有序集合(sorted sets)和位圖(bitmaps)等類型。官方網站是 http://redis.io/
Redis 和其它 NO SQL 的比較本文不做過多闡述。我覺得 Redis 最好的地方就是提供數據持久化功能(定時把內存中的數據寫入文件),從而不至於一旦宕機將造成數據丟失。而且相較於 Memcached ,它提供的值類型選擇更為寬泛。
二、Redis 下載安裝
打開 Redis 官網,我們發現 Redis 官方並不支持 Windows 平台,但 Microsoft Open Tech Group 卻改變了這一情況
點擊 Learn more
點擊 Download ZIP, 下載完后解壓,我們發現其並沒有提供現成的執行安裝文件,這就需要我們自行進行編譯。定位到目錄 Redis\redis2.8\msvs,打開文件 RedisServer.sln
項目結構如下圖
由於筆者的機器為64位,在編譯之前我們確認一下編譯 Platform, 同時我們可以看到對於此 project 將會編譯產生 redis-server.exe 文件
其它項目類似
編譯成功之后,我們到其 Debug 目錄下找到編譯產生的文件
為了便於處理,我們新建目錄 Redis,並把這些文件拷貝過去
其中的 redis.conf 來自如下目錄
至此,我們已經得到所有需要的文件了,下面就可以使用了,打開 CMD, 定位到目錄 D:\Developer\Redis\Redis,然后執行如下命令
redis-server.exe redis.conf
執行成功的截圖(可以看到端口為6379, 進程標識符 PID 為7696)
執行過程中還會讀取配置,由於截圖太長,故這里放出文字

Server 端好了,現在我們開一個 Client 端來測試一下,新打開 CMD (之前打開的 CMD - Server 端不能關閉)
redis-cli.exe -h 10.7.15.172 -p 6379
其中 10.7.15.172 為本機 IP
set hello helloworld
設置一個值
get hello
讀取這個值
大概15分鍾之后我們發現 Server 端也有變化
原來15分鍾自動把內存中的數據寫入 RDF 文件以防丟失。
至於為什么是15分鍾,我們可以看到配置文件是這樣設置的(1個更改/900秒,10更改/300秒,10000更改/60秒),即更改的越多,數據寫入文件的時間間隔越短,這樣設計蠻合理的。
三、Redis Desktop Manager
雖然通過上面的 CMD 我們也能看到 Redis 在內存中的數據,但方式太不友好了,這里介紹一個工具 Redis Desktop Manager
下載完成后安裝,之后連接至 Server 即可
點擊查看數據
四、Install Redis as Windows Service
前面我們通過 CMD 方式安裝了Redis, 但是非常不方便,因為我們要一直保持窗口打開,而且如果機器重啟的話也需要重新打開。Redis 也可以以 Windows Service 的方式進行部署。在部署之前我們需要把配置文件找到
然后拷貝到 Redis 目錄
安裝服務
redis-server --service-install redis.windows.conf
安裝成功提示
查看服務
啟動服務
redis-server --service-start
停止服務
redis-server --service-stop
卸載服務
redis-server --service-uninstall
其實安裝成 Windows 服務還有一種方式,就是從 Github 上直接下載安裝文件,但是好像不是最新的版本
二、Redis中的數據類型
redis是鍵值對的數據庫,有5中主要數據類型:
字符串類型(string),散列類型(hash),列表類型(list),集合類型(set),有序集合類型(zset)
幾個基本的命令:
KEYS * 獲得當前數據庫的所有鍵
EXISTS key [key ...] 判斷鍵是否存在,返回個數,如果key有一樣的也是疊加數
DEL key [key ...] 刪除鍵,返回刪除的個數
TYPE key 獲取減值的數據類型(string,hash,list,set,zset)
FLUSHALL 清空所有數據庫
CONFIG [get、set] redis配置
-inf 負無窮
+inf正無窮
一:字符串類型string
字符串類型是Redis的最基本類型,它可以存儲任何形式的字符串。其它的四種類型都是字符串類型的不同形式。
最基本的命令:GET、SET 語法:GET key,SET key value value如果有空格需要雙引號以示區分
整數遞增:INCR 語法:INCR key 默認值為0,所以首先執行命令得到 1 ,不是整型提示錯誤
增加指定的整數:INCRBY 語法:INCRBY key increment
整數遞減:DECR 語法:DECR key 默認值為0,所以首先執行命令得到 -1,不是整型提示錯誤
減少指定的整數:DECRBY 語法:DECRBY key increment
增加指定浮點數:INCRBYFLOAT 語法:INCRBYFLOAT key increment 與INCR命令類似,只不過可以遞增一個雙精度浮點數
向尾部追加值:APPEND 語法:APPEND key value redis客戶端並不是輸出追加后的字符串,而是輸出字符串總長度
獲取字符串長度:STRLEN 語法:STRLEN key 如果鍵不存在返回0,注意如果有中文時,一個中文長度是3,redis是使用UTF-8編碼中文的
獲取多個鍵值:MGET 語法:MGET key [key ...] 例如:MGET key1 key2
設置多個鍵值:MSET 語法:MSET key value [key value ...] 例如:MSET key1 1 key2 "hello redis"
二進制指定位置值:GETBIT 語法:GETBIT key offset 例如:GETBIT key1 2 ,key1為hello 返回 1,返回的值只有0或1,
當key不存在或超出實際長度時為0
設置二進制位置值:SETBIT 語法:SETBIT key offset value ,返回該位置的舊值
二進制是1的個數:BITCOUNT 語法:BITCOUNT key [start end] ,start 、end為開始和結束字節
位運算:BITOP 語法:BITOP operation destkey key [key ...] ,operation支持AND、OR、XOR、NOT
偏移:BITPOS 語法:BITPOS key bit [start] [end]
二:散列類型hash
設置單個:HSET 語法:HSET key field value,不存在時返回1,存在時返回0,沒有更新和插入之分
設置多個:HMSET 語法:HMSET key field value [field value ...]
讀取單個:HGET 語法:HGET key field,不存在是返回nil
讀取多個:HMGET 語法:HMGET key field [field ...]
讀取全部:HGETALL 語法:HGETALL key,返回時字段和字段值的列表
判斷字段是否存在:HEXISTS 語法:HEXISTS key field,存在返回1 ,不存在返回0
字段不存在時賦值:HSETNX 語法:HSETNX key field value,與hset命令不同,hsetnx是鍵不存在時設置值
增加數字:HINCRBY 語法:HINCRBY key field increment ,返回增加后的數,不是整數時會提示錯誤
刪除字段:HDEL 語法:HDEL key field [field ...] ,返回被刪除字段的個數
只獲取字段名:HKEYS 語法:HKEYS key ,返回鍵的所有字段名
只獲取字段值:HVALS 語法:HVALS key ,返回鍵的所有字段值
字段數量:HLEN 語法:HLEN key ,返回字段總數
三:列表類型(list)
內部使用雙向鏈表實現,所以獲取越接近兩端的元素速度越快,但通過索引訪問時會比較慢
添加左邊元素:LPUSH 語法:LPUSH key value [value ...] ,返回添加后的列表元素的總個數
添加右邊元素:RPUSH 語法:RPUSH key value [value ...] ,返回添加后的列表元素的總個數
移除左邊第一個元素:LPOP 語法:LPOP key ,返回被移除的元素值
移除右邊第一個元素:RPOP 語法:RPOP key ,返回被移除的元素值
列表元素個數:LLEN 語法:LLEN key, 不存在時返回0,redis是直接讀取現成的值,並不是統計個數
獲取列表片段:LRANGE 語法:LRANGE key start stop,如果start比stop靠后時返回空列表,0 -1 返回整個列表
正數時:start 開始索引值,stop結束索引值(索引從0開始)
負數時:例如 lrange num -2 -1,-2表示最右邊第二個,-1表示最右邊第一個,
刪除指定值:LREM 語法:LREM key count value,返回被刪除的個數
count>0,從左邊開始刪除前count個值為value的元素
count<0,從右邊開始刪除前|count|個值為value的元素
count=0,刪除所有值為value的元素
索引元素值:LINDEX 語法:LINDEX key index ,返回索引的元素值,-1表示從最右邊的第一位
設置元素值:LSET 語法:LSET key index value
保留列表片段:LTRIM 語法:LTRIM key start stop,start、top 參考lrange命令
一個列表轉移另一個列表:RPOPLPUSH 語法:RPOPLPUSH source desctination ,從source列表轉移到desctination列表,
該命令分兩步看,首先source列表RPOP右移除,再desctination列表LPUSH
四:集合類型(set)
集合類型值具有唯一性,常用操作是向集合添加、刪除、判斷某個值是否存在,集合內部是使用值為空的散列表實現的。
添加元素:SADD 語法:SADD key member [member ...] ,向一個集合添加一個或多個元素,因為集合的唯一性,所以添加相同值時會被忽略。
返回成功添加元素的數量。
刪除元素:SREM 語法:SREM key member [member ...] 刪除集合中一個或多個元素,返回成功刪除的個數。
獲取全部元素:SMEMBERS 語法:SMEMBERS key ,返回集合全部元素
值是否存在:SISMEMBER 語法:SISMEMBER key member ,如果存在返回1,不存在返回0
差運算:SDIFF 語法:SDIFF key [key ...] ,例如:集合A和集合B,差集表示A-B,在A里有的元素B里沒有,返回差集合;多個集合(A-B)-C
交運算:SINTER 語法:SINTER key [key ...],返回交集集合,每個集合都有的元素
並運算:SUNION 語法:SUNION key [key ...],返回並集集合,所有集合的元素
集合元素個數:SCARD 語法:SCARD key ,返回集合元素個數
集合運算后存儲結果 語法:SDIFFSTROE destination key [key ...] ,差運算並存儲到destination新集合中
SINTERSTROE destination key [key ...],交運算並存儲到destination新集合中
SUNIONSTROE destination key [key ...],並運算並存儲到destination新集合中
隨機獲取元素:SRANDMEMGER 語法:SRANDMEMBER key [count],根據count不同有不同結果,count大於元素總數時返回全部元素
count>0 ,返回集合中count不重復的元素
count<0,返回集合中count的絕對值個元素,但元素可能會重復
彈出元素:SPOP 語法:SPOP key [count] ,因為集合是無序的,所以spop會隨機彈出一個元素
五:有序集合類型
添加集合元素:ZADD 語法:ZADD key [NX|XX] [CH] [INCR] score member [score member ...],不存在添加,存在更新。
獲取元素分數:ZSCORE 語法:ZSCORE key member ,返回元素成員的score 分數
元素小到大:ZRANGE 語法:ZRANGE key start top [WITHSCORES] ,參考LRANGE ,加上withscores 返回帶元素,即元素,分數
當分數一樣時,按元素排序
元素大到小:ZREVRANGE 語法:ZREVRANGE key start [WITHSCORES] ,與zrange區別在於zrevrange是從大到小排序
指定分數范圍元素:ZRANGEBYSCORE 語法:ZRANGEBYSCORE key min max [WITHSCORE] [LIMIT offest count]
返回從小到大的在min和max之間的元素,( 符號表示不包含,例如:80-100,(80 100,
withscore返回帶分數
limit offest count 向左偏移offest個元素,並獲取前count個元素
指定分數范圍元素:ZREVRANGESCORE 語法:ZREVRANGEBYSCORE key max min [WITHSCORE] [LIMIT offest count]
與zrangebyscore類似,只不過該命令是從大到小排序的。
增加分數:ZINCRBY 語法:ZINCRBY key increment member ,注意是增加分數,返回增加后的分數;如果成員不存在,則添加一個為0的成員。
三、GO中Redis簡使用
連接
import "github.com/garyburd/redigo/redis" func main() { c, err := redis.Dial("tcp", "localhost:6379") if err != nil { fmt.Println("conn redis failed, err:", err) return } defer c.Close() }
set & get
_, err = c.Do("Set", "name", "nick") if err != nil { fmt.Println(err) return } r, err := redis.String(c.Do("Get", "name")) if err != nil { fmt.Println(err) return } fmt.Println(r)
mset & mget
批量設置
_, err = c.Do("MSet", "name", "nick", "age", "18") if err != nil { fmt.Println("MSet error: ", err) return } r2, err := redis.Strings(c.Do("MGet", "name", "age")) if err != nil { fmt.Println("MGet error: ", err) return } fmt.Println(r2)
hset & hget
hash操作
_, err = c.Do("HSet", "names", "nick", "suoning") if err != nil { fmt.Println("hset error: ", err) return } r, err = redis.String(c.Do("HGet", "names", "nick")) if err != nil { fmt.Println("hget error: ", err) return } fmt.Println(r)
expire
設置過期時間
_, err = c.Do("expire", "names", 5) if err != nil { fmt.Println("expire error: ", err) return }
lpush & lpop & llen
隊列
// 隊列 _, err = c.Do("lpush", "Queue", "nick", "dawn", 9) if err != nil { fmt.Println("lpush error: ", err) return } for { r, err = redis.String(c.Do("lpop", "Queue")) if err != nil { fmt.Println("lpop error: ", err) break } fmt.Println(r) } r3, err := redis.Int(c.Do("llen", "Queue")) if err != nil { fmt.Println("llen error: ", err) return }
連接池
各參數的解釋如下:
MaxIdle:最大的空閑連接數,表示即使沒有redis連接時依然可以保持N個空閑的連接,而不被清除,隨時處於待命狀態。
MaxActive:最大的激活連接數,表示同時最多有N個連接
IdleTimeout:最大的空閑連接等待時間,超過此時間后,空閑連接將被關閉
pool := &redis.Pool{ MaxIdle: 16, MaxActive: 1024, IdleTimeout: 300, Dial: func() (redis.Conn, error) { return redis.Dial("tcp", "localhost:6379") }, }
連接池栗子
package main import ( "fmt" "github.com/garyburd/redigo/redis" ) var pool *redis.Pool func init() { pool = &redis.Pool{ MaxIdle: 16, MaxActive: 1024, IdleTimeout: 300, Dial: func() (redis.Conn, error) { return redis.Dial("tcp", "localhost:6379") }, } } func main() { c := pool.Get() defer c.Close() _, err := c.Do("Set", "name", "nick") if err != nil { fmt.Println(err) return } r, err := redis.String(c.Do("Get", "name")) if err != nil { fmt.Println(err) return } fmt.Println(r) }
管道操作
請求/響應服務可以實現持續處理新請求,客戶端可以發送多個命令到服務器而無需等待響應,最后在一次讀取多個響應。
使用Send(),Flush(),Receive()方法支持管道化操作
Send向連接的輸出緩沖中寫入命令。
Flush將連接的輸出緩沖清空並寫入服務器端。
Recevie按照FIFO順序依次讀取服務器的響應。
func main() { c, err := redis.Dial("tcp", "localhost:6379") if err != nil { fmt.Println("conn redis failed, err:", err) return } defer c.Close() c.Send("SET", "name1", "sss1") c.Send("SET", "name2", "sss2") c.Flush() v, err := c.Receive() fmt.Printf("v:%v,err:%v\n", v, err) v, err = c.Receive() fmt.Printf("v:%v,err:%v\n", v, err) v, err = c.Receive() // 夯住,一直等待 fmt.Printf("v:%v,err:%v\n", v, err)
四、Redis的並發
在日常的開發中,有時我們會遇到這樣的場景:多個人對同一個數據進行修改操作,導致並發問題發生。這個問題可以通過悲觀鎖來解決,但是悲觀鎖也是有限制的,在某些場景中是不適應的,因為和數據的耦合度太高了,可能會影響到其他業務的操作。而使用redis來解決這一問題是很好的選擇。
原理介紹
redis的存儲指令中有一個setnx方法,這個方法有一個特性,就是當鍵不存在的時候,會將這條數據插入,並且返回1,如果這個鍵已經存在了,那么就不會插入這條數據,並且返回0。
功能實現
明白了這個實現的原理之后,要實現這個功能就很簡單了。
- 在事務開啟的時候,我們就去redis中setnx一條數據,這條數據的鍵要和你當前操作的數據有關,這樣就只會鎖定一條數據,而不影響其他數據的業務,例如:做訂單審核的時候,將訂單號+業務簡寫作為鍵。
- 判斷上面插入操作的返回值,如果返回1,就繼續執行,如果返回0,直接return.
- 在事務結束之后,將redis中的這條數據刪除。直接使用del(String key)就可以了。
go操作Redis不得不提就是Pipelining(管道)
管道操作可以理解為並發操作,並通過Send(),Flush(),Receive()三個方法實現。客戶端可以使用send()方法一次性向服務器發送一個或多個命令,命令發送完畢時,使用flush()方法將緩沖區的命令輸入一次性發送到服務器,客戶端再使用Receive()方法依次按照先進先出的順序讀取所有命令操作結果。
Send(commandName string, args ...interface{}) error Flush() error Receive() (reply interface{}, err error)
- Send:發送命令至緩沖區
- Flush:清空緩沖區,將命令一次性發送至服務器
- Recevie:依次讀取服務器響應結果,當讀取的命令未響應時,該操作會阻塞。
示例:
package main import ( "github.com/garyburd/redigo/redis" "fmt" ) func main() { conn,err := redis.Dial("tcp","10.1.210.69:6379") if err != nil { fmt.Println("connect redis error :",err) return } defer conn.Close() conn.Send("HSET", "student","name", "wd","age","22") conn.Send("HSET", "student","Score","100") conn.Send("HGET", "student","age") conn.Flush() res1, err := conn.Receive() fmt.Printf("Receive res1:%v \n", res1) res2, err := conn.Receive() fmt.Printf("Receive res2:%v\n",res2) res3, err := conn.Receive() fmt.Printf("Receive res3:%s\n",res3) } //Receive res1:0 //Receive res2:0 //Receive res3:22
事務操作
MULTI, EXEC,DISCARD和WATCH是構成Redis事務的基礎,當然我們使用go語言對redis進行事務操作的時候本質也是使用這些命令。
MULTI:開啟事務
EXEC:執行事務
DISCARD:取消事務
WATCH:監視事務中的鍵變化,一旦有改變則取消事務。
示例:
package main import ( "github.com/garyburd/redigo/redis" "fmt" ) func main() { conn,err := redis.Dial("tcp","10.1.210.69:6379") if err != nil { fmt.Println("connect redis error :",err) return } defer conn.Close() conn.Send("MULTI") conn.Send("INCR", "foo") conn.Send("INCR", "bar") r, err := conn.Do("EXEC") fmt.Println(r) } //[1, 1]
四、Redis的落地
Redis 的落地策略其實就是持久化(Persistence),主要有以下2種策略:
- RDB: 定時快照方式(snapshot)
- AOF: 基於語句追加文件的方式
RDB
RDB 文件非常緊湊,它保存了 Redis 某個時間點上的數據集。RDB 恢復大數據集時速度要比 AOF 快。但是 RDB 不適合那些對時效性要求很高的業務,因為它只保存了快照,在進行恢復時會導致一些時間內的數據丟失。實際在進行備份時,Redis 主要依靠 rdbSave()
函數,然后有兩個命令會調用這個函數 SAVE
和 BGSAVE
,前者會同步調用,阻塞主進程導致會有短暫的 Redis-server 停止工作,后者會 fork 出子進程異步處理。
在調用 SAVE
或者 BGSAVE
時,只有發布和訂閱功能的命令可以正常執行,因為這個模塊和服務器的其他模塊是隔離的。
下面的命令表示: “60 秒內有至少有 1000 個鍵被改動”時進行RDB文件備份。
redis-server> SAVE 60 1000
RDB 文件的結構
開頭的REDIS
表示這是一個 RDB 文件,然后緊跟着 redis 的版本號,SELECT-DB
和 KEY-VALUES-PAIRS
構成了對一個數據庫中的所有數據記錄,其中 KEY-VALUES-PAIRS
具體結構如下,后面兩個就不用說了。
其中對於不同的類型,RDB文件中有不同的 layout,具體就不寫出來了。
AOF
AOF 可以通過設置的 fsync 策略配置,如果未設置 fsync ,AOF 的默認策略為每秒鍾 fsync 一次,在這種配置下, fsync 會在后台線程執行,所以主線程不會受到打擾。但是像 AOF 這種策略會導致追加的文件非常大,而且在恢復大數據時非常緩慢,因為要把所有會導致寫數據庫的命令都重新執行一遍。AOF文件中實際存儲的是 Redis 協議下的命令記錄,因此非常易讀。
當然 Redis 考慮到了 AOF 文件過大的問題,因此引入了 BGREWRITEAOF
命令進行重建 AOF 文件,保證可以減少大量無用的重復寫操作。重建命令並不會去分析已有的 AOF 文件,而是將當前數據庫的快照保存。
在 AOF 文件重寫時,Redis 的具體邏輯如下:
- Redis 首先 fork 出一個子進程,子進程將新 AOF 文件的內容寫入到臨時文件。
- 對於所有新執行的寫入命令,父進程一邊將它們累積到一個緩存中,一邊將這些改動追加到現有 AOF 文件的末尾: 這樣即使在重寫的中途發生停機,現有的 AOF 文件也還是安全的。
- 當子進程完成重寫工作時,它給父進程發送一個信號,父進程在接收到信號之后,將緩存中的所有數據追加到新 AOF 文件的末尾。
- 現在 Redis 原子地用新文件替換舊文件,之后所有命令都會直接追加到新 AOF 文件的末尾。
Redis 會維持一個默認的AOF重寫策略,當當前的AOF文件比上次重寫之后的文件大小增大了一倍時,就會自動在后台重寫AOF。