redis應用
redis應用
介紹
官網:redis.io
REmote DIctionary Server(Redis) 是一個由Salvatore Sanfilippo寫的key-value儲存系統。
Redis是現在最受歡迎的NoSQL資料庫之一,Redis是一個使用ANSI C編寫的開源、包含多種資料結構、支援網路、基於記憶體、可選永續性的鍵值對儲存資料庫,其具備如下特性:
- 基於記憶體執行,效能高效
- 支援分散式,理論上可以無限擴充套件
- key-value儲存系統
- 開源的使用ANSI C語言編寫、遵守BSD協議、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API
相比於其他資料庫型別,Redis具備的特點是:
- C/S通訊模型
- 單程序單執行緒模型
- 豐富的資料型別
- 操作具有原子性
- 持久化
- 高併發讀寫
- 支援lua指令碼
redis單執行緒問題
所謂的單執行緒指的是網路請求模組使用了一個執行緒(所以不需考慮併發安全性),即一個執行緒處理所有網路請求,其他模組仍用了多個執行緒。
redis採用多路複用機制:即多個網路socket複用一個io執行緒,實際是單個執行緒通過記錄跟蹤每一個Sock(I/O流)的狀態來同時管理多個I/O流.
Redis應用:token生成、session共享、分散式鎖、自增id、驗證碼等。
安裝
Linux下安裝
$ wget http://download.redis.io/releases/redis-2.8.17.tar.gz
$ tar xzf redis-2.8.17.tar.gz
$ cd redis-2.8.17
$ make
$ cd src
$ ./redis-server
$ ./redis-server ../redis.conf
make完後 redis-2.8.17目錄下會出現編譯後的redis服務程式redis-server,還有用於測試的客戶端程式redis-cli,兩個程式位於安裝目錄 src 目錄下。
redis.conf 是一個預設的配置檔案。我們可以根據需要使用自己的配置檔案。
ubuntu安裝
$sudo apt-get update
$sudo apt-get install redis-server
$ redis-server
redis-cli使用
$ redis-cli -h host -p port -a password //遠端
$ redis-cli
127.0.0.1:6379> auth 123456 // 預設沒有密碼,當設定密碼時需要auth
OK
redis 127.0.0.1:6379>ping
PONG
127.0.0.1:6379> help
redis-cli 3.0.6
Type: "help @<group>" to get a list of commands in <group>
"help <command>" for help on <command>
"help <tab>" to get a list of possible help topics // 按tab可以切換不同topics
"quit" to exit
基礎
redis通常被稱為資料結構伺服器,因為值(value)可以是 字串(String), 雜湊(Hash), 列表(list), 集合(sets) 和 有序集合(sorted sets)等型別。
String型別
它是一個二進位制安全的字串,意味著它不僅能夠儲存字串、還能儲存圖片、視訊等多種型別, 最大長度支援512M。
支援的命令:SET、GET
127.0.0.1:6379> set wstrings wang
OK
127.0.0.1:6379> get wstrigns
(nil)
127.0.0.1:6379> get wstrings
"wang"
雜湊型別
該型別是由field和關聯的value組成的map,特別適合儲存物件。其中,field和value都是字串型別的。
支援的命令: hmset、hget、hgetall、hkeys
127.0.0.1:6379> hmset whash f1 v1 f2 v2 name wang id 100 score 100
OK
127.0.0.1:6379> hgetall whash
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "name"
6) "wang"
7) "id"
8) "100"
9) "score"
10) "100"
127.0.0.1:6379> hget whash f1
"v1"
127.0.0.1:6379> hget whash name
"wang"
127.0.0.1:6379> hget whash score
"100"
列表型別
該型別是一個插入順序排序的字串元素集合, 基於雙鏈表實現。
支援的命令:lpush、rpush、lrange、llen
127.0.0.1:6379> lpush wlist redis
(integer) 1
127.0.0.1:6379> lpush wlist mongodb
(integer) 2
127.0.0.1:6379> rpush wlist mysql
(integer) 3
127.0.0.1:6379> lrange wlist 0 3
1) "mongodb"
2) "redis"
3) "mysql"
集合型別
Set型別是一種無順序集合, 它和List型別最大的區別是:集合中的元素沒有順序, 且元素是唯一的。Set型別的底層是通過雜湊表實現的。Set型別主要應用於:在某些場景,如社交場景中,通過交集、並集和差集運算,通過Set型別可以非常方便地查詢共同好友、共同關注和共同偏好等社交關係。
支援的命令:sadd、smembers
127.0.0.1:6379> sadd wset redis
(integer) 1
127.0.0.1:6379> sadd wset mysql
(integer) 1
127.0.0.1:6379> sadd wset redis
(integer) 0
127.0.0.1:6379> smembers wset
1) "redis"
2) "mysql"
127.0.0.1:6379> scard wset
(integer) 2
順序集合型別
ZSet是一種有序集合型別,每個元素都會關聯一個double型別的分數權值,通過這個權值來為集合中的成員進行從小到大的排序。與Set型別一樣,其底層也是通過雜湊表實現的。
支援的命令:zadd、zrange、zcard
127.0.0.1:6379> zadd wzset 0 redis
(integer) 1
127.0.0.1:6379> zadd wzset 3 mysql
(integer) 1
127.0.0.1:6379> zadd wzset 2 mongodb
(integer) 1
127.0.0.1:6379> zcard wzset
(integer) 3
127.0.0.1:6379> zrange wzset 0 3
1) "redis"
2) "mongodb"
3) "mysql"
127.0.0.1:6379> zrange wzset 0 4
1) "redis"
2) "mongodb"
3) "mysql"
127.0.0.1:6379> zrange wzset 0 4 withscores
1) "redis"
2) "0"
3) "mongodb"
4) "2"
5) "mysql"
6) "3"
key命令
支援的命令:keys, type, del, exists
127.0.0.1:6379> keys *
1) "chen"
2) "www"
3) "get"
4) "runoob"
5) "wwang"
127.0.0.1:6379> type chen
list
127.0.0.1:6379> type get
hash
127.0.0.1:6379> del www
(integer) 1
127.0.0.1:6379> exists www
(integer) 0
釋出訂閱
Redis 釋出訂閱(pub/sub)是一種訊息通訊模式:傳送者(pub)傳送訊息,訂閱者(sub)接收訊息。客戶端可以訂閱任意數量的頻道。
支援的命令: subscribe、publish
127.0.0.1:6379> subscribe redischat
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redischat"
3) (integer) 1
1) "message"
2) "redischat"
3) "redis is a great caching technique"
1) "message"
2) "redischat"
3) "redis is nosql db"
// another client
127.0.0.1:6379> publish redischat "redis is a great caching technique"
(integer) 1
127.0.0.1:6379> publish redischat "redis is nosql db"
(integer) 1
golang驅動
推薦go-redis和redigo庫,其中edgex使用了redigo庫,go-redis封裝好。
github.com/go-redis/redis/v8
github.com/gomodule/redigo/redis
// main.go
package main
import (
_ "fmt"
"log"
"time"
"testredis/gredis"
)
const RNETWORK = "tcp"
const RPASSWD = "123456"
const RADDRESS = "172.61.1.240:6379"
const RKEY = "wstring"
func main() {
cli, err := gredis.NewClient(RNETWORK, RADDRESS, RPASSWD)
if err != nil {
log.Fatal(err)
}
defer cli.Close()
log.Println("Client create...")
if _, err = cli.Exists(RKEY); err != nil {
log.Fatal(err)
}
{
log.Println(RKEY + " exists")
data, err := cli.Get(RKEY)
if err != nil {
log.Println(err)
} else {
log.Println("old data: ", data)
}
}
cli.Delete(RKEY)
cli.Set(RKEY, "CHINA", 3600)
data, _ := cli.Get(RKEY)
log.Println("new data: ", data)
time.Sleep(1 * time.Second)
}
// gredis/redis.go
package gredis
import (
_ "encoding/json"
"errors"
"log"
"sync"
"time"
"github.com/gomodule/redigo/redis"
)
var once sync.Once
type Client struct {
Pool *redis.Pool
}
func NewClient(network, address, passwd string) (*Client, error) {
var redisClient Client
once.Do(func() {
redisClient = Client{
Pool: &redis.Pool{
MaxIdle: 10, // Maximum number of idle connections in the pool
MaxActive: 10, // Maximum number of connections allocated by the poll at a given time.
IdleTimeout: 10 * time.Second, // close connection
Dial: func() (redis.Conn, error) {
c, err := redis.Dial(network, address)
if err != nil {
return nil, err
}
if passwd != "" {
if _, err := c.Do("AUTH", passwd); err != nil {
c.Close()
return nil, err
}
}
return c, nil
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
},
}
})
return &redisClient, nil
}
func (c *Client) Set(key string, data interface{}, time int) (err error) {
conn := c.Pool.Get()
defer conn.Close()
// value, err := json.Marshal(data)
value, ok := data.(string)
if !ok {
return errors.New("Set No string")
}
_, err = conn.Do("SET", key, value)
if err != nil {
return err
}
_, err = conn.Do("EXPIRE", key, time)
if err != nil {
return err
}
return nil
}
func (c *Client) Exists(key string) (bool, error) {
conn := c.Pool.Get()
defer conn.Close()
exists, err := redis.Bool(conn.Do("EXISTS", key))
if err != nil {
log.Println(err)
return false, err
}
return exists, nil
}
func (c *Client) Get(key string) (string, error) {
conn := c.Pool.Get()
defer conn.Close()
reply, err := redis.Bytes(conn.Do("GET", key))
if err != nil {
return "", err
}
return string(reply), nil
}
func (c *Client) Delete(key string) (bool, error) {
conn := c.Pool.Get()
defer conn.Close()
return redis.Bool(conn.Do("DEL", key))
}
func (c *Client) LikeDeletes(key string) error {
conn := c.Pool.Get()
defer conn.Close()
keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
if err != nil {
return err
}
for _, key := range keys {
_, err = c.Delete(key)
if err != nil {
return err
}
}
return nil
}
func (c *Client) Close() {
c.Pool.Close()
once = sync.Once{}
}
問題
1. redis的過期策略以及記憶體淘汰機制
分析:這個問題其實相當重要,到底redis有沒用到家,這個問題就可以看出來。比如你redis只能存5G資料,可是你寫了10G,那會刪5G的資料。怎麼刪的,這個問題思考過麼?還有,你的資料已經設定了過期時間,但是時間到了,記憶體佔用率還是比較高,有思考過原因麼?
回答:redis採用的是定期刪除+惰性刪除策略。
為什麼不用定時刪除策略?
定時刪除,用一個定時器來負責監視key,過期則自動刪除。雖然記憶體及時釋放,但是十分消耗CPU資源。在大併發請求下,CPU要將時間應用在處理請求,而不是刪除key,因此沒有采用這一策略.
定期刪除+惰性刪除是如何工作的呢?
定期刪除,redis預設每個100ms檢查,是否有過期的key,有過期key則刪除。需要說明的是,redis不是每個100ms將所有的key檢查一次,而是隨機抽取進行檢查(如果每隔100ms,全部key進行檢查,redis豈不是卡死)。因此,如果只採用定期刪除策略,會導致很多key到時間沒有刪除。
於是,惰性刪除派上用場。也就是說在你獲取某個key的時候,redis會檢查一下,這個key如果設定了過期時間那麼是否過期了?如果過期了此時就會刪除。
採用定期刪除+惰性刪除就沒其他問題了麼?
不是的,如果定期刪除沒刪除key。然後你也沒即時去請求key,也就是說惰性刪除也沒生效。這樣,redis的記憶體會越來越高。那麼就應該採用記憶體淘汰機制。
在redis.conf中有一行配置
# maxmemory-policy allkeys-lru
該配置就是配記憶體淘汰策略的(什麼,你沒配過?好好反省一下自己)
1)noeviction:當記憶體不足以容納新寫入資料時,新寫入操作會報錯。應該沒人用吧。
2)allkeys-lru:當記憶體不足以容納新寫入資料時,在鍵空間中,移除最近最少使用的key。推薦使用。
3)allkeys-random:當記憶體不足以容納新寫入資料時,在鍵空間中,隨機移除某個key。應該也沒人用吧,你不刪最少使用Key,去隨機刪。
4)volatile-lru:當記憶體不足以容納新寫入資料時,在設定了過期時間的鍵空間中,移除最近最少使用的key。這種情況一般是把redis既當快取,又做持久化儲存的時候才用。不推薦
5)volatile-random:當記憶體不足以容納新寫入資料時,在設定了過期時間的鍵空間中,隨機移除某個key。依然不推薦
6)volatile-ttl:當記憶體不足以容納新寫入資料時,在設定了過期時間的鍵空間中,有更早過期時間的key優先移除。不推薦
ps:如果沒有設定 expire 的key, 不滿足先決條件(prerequisites); 那麼 volatile-lru, volatile-random 和 volatile-ttl 策略的行為, 和 noeviction(不刪除) 基本上一致。
2. redis和資料庫雙寫一致性問題
分析:一致性問題是分散式常見問題,還可以再分為最終一致性和強一致性。資料庫和快取雙寫,就必然會存在不一致的問題。答這個問題,先明白一個前提。就是如果對資料有強一致性要求,不能放快取。我們所做的一切,只能保證最終一致性。另外,我們所做的方案其實從根本上來說,只能說降低不一致發生的概率,無法完全避免。因此,有強一致性要求的資料,不能放快取。
回答:首先,採取正確更新策略,先更新資料庫,再刪快取。其次,因為可能存在刪除快取失敗的問題,提供一個補償措施即可,例如利用訊息佇列。
參考:
1 Redis 教程 runoob
5 golang中使用redis 簡書 推薦go-redis和redigo庫