1. 程式人生 > 實用技巧 >redis應用

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下安裝

可從http://redis.io/download

下載最新穩定版本,並安裝。

$ 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

2 一文看懂redis

3 redis全面解析

4 用 Go 來了解一下 Redis 通訊協議 煎魚

5 golang中使用redis 簡書 推薦go-redis和redigo庫