1. 什么是NoSql
NoSQL一詞最早出現于1998年,是Carlo Strozzi開發的一個輕量、開源、不提供SQL功能的關系數據庫。2009年,Last.fm的Johan Oskarsson發起了一次關于分布式開源數據庫的討論,來自Rackspace的Eric Evans再次提出了NoSQL的概念,這時的NoSQL主要指非關系型、分布式、不提供ACID的數據庫設計模式。它不同于傳統的關系數據庫,兩者存在許多顯著的不同點,其中最重要的是NoSQL不使用SQL作為查詢語言。其數據存儲可以不需要固定的表格模式。
2. Redis簡介
2008年,意大利的一家創業公司Merzia推出了一款基于MySQL的網站實時統計系統LLOOGG,然而沒過多久該公司的創始人 Salvatore Sanfilippo便對MySQL的性能感到失望,于是他決定親自為LLOOGG量身定做一個數據庫,并于2009年開發完成,這個數據庫就是Redis。 不過Salvatore Sanfilippo并不滿足只將Redis用于LLOOGG這一款產品,而是希望更多的人使用它,于是在同一年Salvatore Sanfilippo將Redis開源發布,并開始和Redis的另一名主要的代碼貢獻者Pieter Noordhuis一起繼續著Redis的開發,直到今天。短短的幾年時間,Redis就擁有了龐大的用戶群體。Hacker News在2012年發布了一份數據庫的使用情況調查,結果顯示有近12%的公司在使用Redis。國內如新浪微博、街旁網、知乎網,國外如GitHub、Stack Overflow、Flickr等都是Redis的用戶。VMware公司從2010年開始贊助Redis的開發, Salvatore Sanfilippo和Pieter Noordhuis也分別在3月和5月加入VMware,全職開發Redis。
Redis是使用c語言開發的一個高性能鍵值數據庫。常用于分布式系統中的緩存、電商秒殺、排行榜、訪問量統計、分布式會話共享等高并發應用場景。Redis可以通過一些鍵值類型來存儲數據。其數據類型包括字符類型、散列類型、列表類型、集合類型、有序集合類型。
3. 安裝Redis
訪問Redis官網https://redis.io/download下載最新的版本 。
解壓并編譯安裝
$ tar xzf redis-5.0.3.tar.gz
$ cd redis-5.0.3
$ make install
Redis官網并沒有提供windows版本,但可以前往https://github.com/tporadowski/redis/releases下載windows的個人編譯版本(注意:并不是最新的版本)。
4. 啟動服務
4.1 前端啟動
在redis的src目錄有一個redis-server文件,用于啟動一個redis服務。
redis的默認端口為6379,當客戶端需要連接到redis服務時,就通過服務端的IP地址以及這個端口進行連接。也可以修改這個默認端口。在redis的根目錄下有一個redis.conf文件,它是redis的核心配置文件,redis的所有配置信息都在此文件中。如果需要修改端口,我們在配置文件中找到port配置,并將6379改為其他的端口號。
修改完后需要重新啟動redis服務,需要注意的是,在使用redis-server啟動服務時需要指定redis.conf文件的絕對路徑,否則redis將以默認的配置啟動一個服務實例。
前端啟動的模式我們可以在終端看到redis的啟動信息和相關的操作日志,但此時如果關閉了終端或者使用control+c將會立即停止redis服務。
4.2 后端啟動
所謂后端啟動,就是以一個獨立的進程來運行一個redis服務。首先修改redis.conf文件,找到daemonize選項并設置為yes,如下圖:
保存退出后重新啟動redis服務,此時redis將以后臺進程的方式啟動服務。終端沒有顯示相關的啟動信息,并且啟動完成后,終端可以繼續執行其他的操作。
5. 客戶端連接
5.1 Redis客戶端
在redis的src目錄下有一個redis-cli命令,這個就是官方提供的redis客戶端,可以使用它來連接和操作redis。當然,這僅僅只是一個命令行的客戶端程序,在實際的開發中會有不同的平臺語言,因此官網也提供了對各種語言的客戶端實現,在實際的項目開發中使用不同語言的客戶端來操作redis。例如官網提供了一個Java的客戶端Jedis。
1)使用redis-cli
可以使用使用官方自帶的redis-cli客戶端來連接redis服務。參數-h為連接redis服務器的IP地址,-p為redis的端口號。連接完成后就可以對redis進行操作了。
2)退出客戶端
如果想要退出客戶端的連接只需要在連接的狀態下輸入quit或者exit即可。
3)身份認證
默認連接Redis時是不需要認證密碼的,我們可以為其設置一個連接的認證密碼。首先在redis.conf中找到requirepass配置項,取消注釋并設置一個密碼。
保存后重啟服務,在連接客戶端時加上-a參數并輸入配置的密碼。
連接時也可以不指定密碼也可以正常連接,但在操作Redis時候會提示一個錯誤,要求輸入認證密碼。這時使用auth命令來輸入密碼即可。
5.2 可視化客戶端
也可以使用第三方的redis的可視化客戶端RDM(redis-desktop-manager),它同時提供了各種系統平臺的編譯版本,安裝后即可使用。下載地址:
點擊左上角的Connect to Redis Server,在彈出的窗口中填寫相關的Name(連接名稱)、Address(連接地址)、端口號以及認證密碼(Auth),點擊OK即可。
這里我們看到連接redis后默認有16個庫(0 ~ 15),這是redis默認的配置,可以在redis.conf中可以找到相應的選項并修改默認數量。
當我們使用客戶端連接redis時,默認選擇的是index為0的數據庫,然而也可以使用select命令選擇其他數據庫。例如選擇index為15的數據庫,如下操作:
5.3. 停止服務
如果使用前端啟動redis,可以使用control+c或者kill命令來殺掉進程的方式關閉redis(注意:control+c并不能停止后端啟動的redis),但這些方式都是強制性的關閉redis,由于redis保存的數據先會存儲在內存,如果此時強制關閉,將導致redis還沒將數據持久化到文件中就退出,可能會照成部分的數據丟失。因此,應該使用正常的退出方式來停止redis服務,正常退出redis同樣使用redis-cli工具。
如果設置了認證密碼,在關閉服務端時也同樣需要指定。
6. 數據類型及常用API
Redis支持五種數據類型:string(字符串),hash(哈希),list(列表),set(集合)及sorted set(zset:有序集合)。
6.1 string(字符串)
String 是 redis 最基本的類型,一個 key 對應一個 value。它是二進制安全的,可以包含任何數據,如jpg圖片或者序列化的對象。
1)SET
語法:set key value
賦值操作。
2)GET
語法:get key
取值操作。
3)GETSET
語法:getset key value
取值后重新賦值。
4)MSET
語法:mset key value [key value …]
同時設置多個鍵值。
5)MGET
語法:mget key [key …]
獲取多個鍵值。
6)DEL
語法:del key [key …]
刪除一個或多個鍵值對。
7)INCR
語法:incr key
當存儲的字符串是整數時,讓當前鍵值遞增,并返回遞增或增加后的值。
8)INCRBY
語法:incrby key increment*
當存儲的字符串是整數時,讓當前鍵值增加指定的數值,并返回遞增或增加后的值。
9)DECR
語法:decr key
讓當前鍵值遞減,并返回遞減或減少后的值。
10)DECRBY
語法:decrby key decrement
讓當前鍵值減少指定的數值,并返回遞減或減少后的值。
11)APPEND
語法:append key value
向鍵值的末尾追加value。如果鍵不存在則將該鍵的值設置為value,即相當于 SET
key value。返回值是追加后字符串的總長度。
12)獲取字符串長度(STRLEN)
STRLEN命令返回鍵值的長度,如果鍵不存在則返回0。
6.2 hash(哈希)
hash是一個string類型的field和value的映射表,而field只能是String類型,hash特別適合用于存儲對象。
1)HSET
語法:HSET key field value
HSET一次只能設置一個字段值。HSET命令不區分插入和更新操作,當執行插入操作時HSET命令返回1,當執行更新操作時返回0。
2)HSETNX
語法:HSETNX key field value
當字段不存在時賦值,類似HSET。區別在于如果字段存在,該命令不執行任何操作。
例如:hsetnx user name zing
說明:如果user中不存在name字段則設置name的值為zing,否則不做任何操作。
3)HGET
語法:HGET key field
HGET一次只能獲取一個字段值。
4)HGETALL
語法:HGETALL key
獲取所有字段值。
5)HDEL
語法:HDEL key field [field…]
可以刪除一個或多個字段,返回值是被刪除的字段個數。
6)HINCRBY
語法:HINCRBY key field increment
為某個字段增加數值。
7)HEXISTS
語法:HEXISTS key field
判斷字段是否存在,存在則返回1,否則返回0。
8)HKEYS
語法:HKEYS key
獲取所有的字段名。
9)HVALS
語法:HVALS key
獲取所有字段的值。
10)HLEN
語法:HLEN key
獲取字段數量。
6.3 list(列表)
Redis的list是采用來鏈表來存儲的,所以對于Redis的list數據類型的操作,是操作list的兩端數據來操作的。
1)LPUSH
語法:LPUSH key value [value …]
向列表左邊添加元素。
2)RPUSH
語法:RPUSH key value [value …]
向列表右邊添加元素。
3)LRANGE
語法:LRANGE key start stop
LRANGE命令是列表類型最常用的命令之一,用于獲取列表中的某一片段,將返回start到stop之間的所有元素(包含兩端的元素),索引從0開始。索引可以是負數,如:-1代表最后邊的一個元素。
4)LPOP
語法:LPOP key
LPOP命令從列表左邊彈出一個元素,會分兩步完成:第一步是將列表左邊的元素從列表中移除。第二步是返回被移除的元素值。
5)RPOP
語法:RPOP key
RPOP命令從列表右邊彈出一個元素,步驟與LPOP類似,第一步是將列表右邊的元素從列表中移除。第二步是返回被移除的元素值。
6)LLEN
語法:LLEN key
獲取列表中元素的個數
7)LREM
語法:LREM key count value
LREM命令會刪除列表中前count個值為value的元素,返回實際刪除的元素個數。根據count值的不同,該命令的執行方式會有所不同:
當count>0時, LREM會從列表左邊開始刪除。
當count<0時, LREM會從列表右邊開始刪除。
當count=0時,LREM刪除所有值為value的元素。
8)LINDEX
語法:LINDEX key index
獲得指定索引的元素值。
9)LSET
語法:LSET key index value
設置指定索引的元素值。
10)LTRIM
語法:LTRIM key start stop
只保留列表的指定片段
11)LINSERT
語法:LINSERT key BEFORE|AFTER pivot value
LINSERT首先會在列表中從左到右查找值為pivot的元素,然后根據第二個參數是BEFORE還是AFTER來決定將value插入到該元素的前面還是后面。
12)RPOPLPUSH
語法:RPOPLPUSH source destination
將一個列表的最后一個元素轉移到另一個列表的最前面
6.4 set(集合)
Redis 的 Set 是 String 類型的無序集合。集合成員是唯一的,這就意味著集合中不能出現重復的數據。
1)SADD
語法:SADD key member [member …]
增加一個或多個元素。
2)SREM
語法:SREM key member [member …]
移除一個或多個元素。
3)SMEMBERS
語法:SMEMBERS key
獲得集合中的所有元素。
4)SISMEMBER
語法:SISMEMBER key member
判斷元素是否存在集合中。存在返回1,否則返回0。
5)SDIFF
語法:SDIFF key [key …]
查找屬于集合A并且不屬于集合B的元素。(差集運算)
6)SINTER
語法:SINTER key [key …]
查找屬于集合A且屬于集合B的元素。(交集運算)
7)SUNION
語法:SUNION key [key …]
查找屬于集合A或者屬于集合B的元素。(合并運算)
8)SCARD
語法:SCARD key
獲取集合中元素的個數。
9)SPOP
語法:SPOP key [count]
從集合中彈出一個或多個元素,由count指定。如果不指定count,默認彈出一個。由于集合是無序的,所有SPOP命令會從集合中隨機選擇一個元素彈出。
6.5 zset(有序集合)
zset又稱sorted set,稱之為有序集合,可排序的,但是唯一。和set的不同之處在于zset會給集合中的元素添加一個分數,然后通過這個分數進行排序。
1)ZADD
語法:ZADD key score member [score member …]
向有序集合中加入一個或多個元素和該元素的分數,如果該元素已經存在則會用新的分數替換原有的分數。返回值是新加入到集合中的元素個數,不包含之前已經存在的元素。
2)ZSCORE
語法:ZSCORE key member
獲取元素的分數。
3)ZREM
語法:ZREM key member [member …]
移除有序集合中的一個或多個成員,不存在的成員將被忽略。
4)ZRANGE
語法:ZRANGE key start stop [WITHSCORES]
按照元素分數從小到大的順序返回索引從start到stop之間的所有元素(包含兩端的元素)。如果需要獲得元素的分數可以在命令尾部加上WITHSCORES參數。
5)ZREVRANGE
語法:ZREVRANGE key start stop [WITHSCORES]
按照元素分數從大到小的順序返回索引從start到stop之間的所有元素(包含兩端的元素)。如果需要獲得元素的分數的可以在命令尾部加上WITHSCORES參數。
6)ZRANK
語法:ZRANK key member
獲取元素排名(從小到大)。
7)ZREVRANK
語法:ZREVRANK key member
獲取元素排名(從大到小)。
8)ZRANGEBYSCORE
語法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
獲得指定分數范圍的元素。
9)ZINCRBY
語法:ZINCRBY key increment member
增加某個元素的分數,并返回更改后的分數。
10)ZCARD
語法:ZCARD key
獲取集合元素的數量。
11)ZCOUNT
語法:ZCOUNT key min max
獲取指定分數范圍內的元素個數。
12)ZREMRANGEBYRANK
語法:ZREMRANGEBYRANK key start stop
按照排名范圍刪除元素。
13)ZREMRANGEBYSCORE
語法:ZREMRANGEBYSCORE key min max
按照分數范圍刪除元素。
7. Redis鍵(Keys)
Redis鍵是二進制安全的,這意味著你可以使用任何二進制序列作為鍵,從像”foo” 這樣的字符串到一個 JPEG文件的內容。空字符串也是合法的鍵。
7.1 鍵的一些設計規則
-
不要使用太長的鍵。例如,不要使用一個1024字節的鍵,不僅是因為占用內存,而且在數據集中查找key時需要多次耗時的key比較。
-
不要使用太短的key。例如,user:1001比u1001更具有實際意義,相對于key本身以及value對象來說,增加的空間微乎其微。當然,短的鍵會消耗少的內存,需要找到平衡點。
-
規范一種模式 (schema)。用冒號或者下橫線來連接多單詞字段,例如:”user:1001”或者"user_1001"。
7.2 Key的常用API
1)KEYS
語法:keys pattern
返回指定pattern的所有key
2)EXISTS
語法:exists key
判斷一個key是否存在。存在返回后1,否則返回0。
3)RENAME
語法:rename key newkey
重命名key
4)TYPE
語法:type key
根據key返回value的類型。
5)EXPIRE
語法:expire key seconds
設置key的生存時間。Redis的數據是緩存在內存中的,然后很多時候數據一般都會設置一個過期時間(即到期后銷毀數據,從而釋放更多的內存)。過期時間默認以秒為單位,默認值為-1,表示永不過期。
也可以在設值的時候指定過期時間(秒)
6)TTL
語法:ttl key
查看key剩余的過期時間。
7)PERSIST
語法:persist key
清除key的過期時間。
8)PEXPIRE
語法:pexpire key
以毫秒為單位設置key的過期時間。
也可以在設值的時候指定過期的時間(毫秒)
8. 持久化
8.1 簡介
Redis是一個支持持久化的內存數據庫,可以將內存中的數據同步到磁盤保證持久化。我們知道Redis會將數據緩存在內存中,如果沒有持久化,在服務器關閉或重啟之后數據會丟失。為了保證數據的安全以及效率,Redis會周期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件。而Redis提供了RDB和AOF兩種持久化策略。
8.2 RDB
Redis默認是會以快照RDB的形式將數據持久化到磁盤的一個dump.rdb二進 制文件。當Redis決定要持久化時,會 fork 一個子進程將數據寫到磁盤上一個臨時的RDB文件中,當子進程完成寫操作后,將原來的RDB替換掉。而Redis會在滿足某些條件后會進行持久化,并且可以對其進行配置。
配置RDB
在redis.conf文件中找到“Save the DB on disk”的配置,我們可以根據需要來修改這Redis的RDB持久化策略。
說明:
save 900 1(如果在900秒之內有1次操作,則執行快照保存)
save 300 10(如果在300秒內有10次操作,則執行快照保存)
save 60 10000(如果在1分鐘之內有10000個次操作,則執行快照保存)
SAVE和BGSAVE
我們可以在客戶端直接使用SAVE或者BGSAVE命令立即將Redis的數據持久化到RDB文件中。他們兩者的區別在于BGSAVE命令會fork一個子進程在后臺進行持久化,主進程可以繼續處理客戶端發送的命令(非阻塞)。而SAVE命令需要等待Redis持久化完成后才可以繼續處理客戶端發送的命令(阻塞)。
**RDB優點 **
RDB非常適合用于數據備份, 可以在當天內每小時備份一次,或者每個月的每天都進行備份。 如果遇到斷電或者宕機等其他一些災難情況,可以隨時將數據集還原。
RDB缺點
如果對數據的完整性和安全性要求非常高,要求每一次的操作數據都能持久化到文件中,這時RDB就不太適合了。因為RDB是按照時間范圍的操作次數為條件促發持久化,如果未滿足這些觸發條件,Redis并不會將數據保存到文件,導致數據丟失。例如:save 60 10000,如果在1分鐘之內有9000次的操作,如果此時服務器異常退出或宕機,由于未滿足條件,將導致丟失這1分鐘的數據。
8.3 AOF
AOF持久化可以記錄每個寫操作,將Redis執行過的所有寫指令(讀操作不記錄)保存到appendonly.aof文件中,并且只允許追加文件而不可以改寫文件。在Redis啟動的時候會讀取該文件重新構建緩存數據。在打開AOF持久化機制之后,Redis每當接收到一條寫命令,會先寫入系統緩存,然后每隔一定時間(默認是每秒鐘)fsync一次(寫入到指定文件)。
啟用AOF
AOF持久化默認是關閉的,如果要啟用AOF,需要在redis.conf配置文件中啟用該功能,將appendonly no設置為appendonly yes。
所有寫操作默認保存在appendonly.aof文件中,可以自行修改保存的路徑和文件名。
同步策略
AOF提供了三種同步策略:
- always(每次寫操作就執行一次fsync)
- everysec(每秒執行一次fsync,默認)
- no(不執行fsync)
AOF重寫
AOF會記錄Redis所有的寫操作命令,但這種方式會造成一個問題,就是隨著時間的推移,大量頻繁的操作將導致AOF文件體積的急劇增長,對系統會造成影響。為了解決以上的問題, Redis就需要對AOF文件進行重寫。重寫的過程會創建一個新的AOF文件來代替原有的AOF文件, 而新AOF文件和原有AOF 文件保存的數據狀態是一致的,但新文件的體積將變得盡可能地小。以下兩種方式會觸發AOF重寫。
1)手動出發
在客戶端直接向Redis發送BGREWRITEAOF命令,這個命令會通過移除AOF文件中的冗余命令來重寫(rewrite)AOF文件。
2)自動觸發
其實在啟用了AOF之后(appendonly yes),Redis會依據redis.conf配置文件中的auto-aof-rewrite-percentage選項和auto-aof-rewrite-min-size選項來自動執行BGREWRITEAOF命令。
說明:
例如設置了auto-aof-rewrite-percentage為100和auto-aof-rewrite-min-size為64mb,那么當AOF文件的體積大于64MB時,并且AOF文件的體積比上一次重寫之后的體積大一倍(100%)的,Redis將執行BGREWRITEAOF命令進行重寫。
AOF優點
AOF彌補了RDB按照時間范圍的操作次數為條件的缺點,即使在默認的策略中發生故障,最多也只會丟失一秒鐘的數據,更大程度的保證了數據的安全性。
AOF缺點
AOF會保存每一次的寫操作,這將導致AOF文件的體積通常要大于RDB文件。如果選用always策略,則表示每一次操作都會記錄到AOF文件中,從性能的角度上來說會低于RDB。當然,使用默認的everysec策略進行持久化性能還是非常可觀的。
8.4 混合持久化
在實際應用中,通常會同時使用RDB和AOF兩種持久化來找到一個最佳的平衡點,即能保證性能的同時最大程度保證數據的安全。因此需要RDB和AOF兩者同時進行合理的設置和調整。而從Redis 4.0開始,官方提供了一種更加方便的混合持久化配置。
未啟用混合持久化
在未啟用混合持久化之前,如果我們往Redis寫入一條記時,RDB文件會保存操作的鍵值數據,AOF文件則保存的是寫操作的指令,我們可以分別查看一下這兩個文件的內容。
使用cat命令查看RDB文件
然而顯示的內容并不太直觀也不易理解,因此可以借助Redis提供的redis-check-rdb工具進行查看。
RDB文件中會保存Redis的相關信息以及存儲的keys數量和相關的活期時間。接下來我們繼續查看AOF文件的內容,直接使用cat命名進行查看。
結果顯示AOF文件中保存的是相關的操作指令。
混合持久化
要使用混合持久化,除了在redis.conf文件中啟用AOF(將appendonly設置為yes),還需要將aof-use-rdb-preamble設置為yes。
設置完重新啟用Redis服務。啟用了混合持久化之后,使用BGREWRITEAOF命令執行一次AOF重寫,同時向Redis插入一條新的數據。
然后再次使用cat命令再次查看AOF文件,這時會發現啟用混合持久化之后的AOF文件內容和未啟用時的AOF文件內容不一樣。這是因為此時產生的AOF文件是一個RDB-AOF的混合文件,Redis會基于某種協議將此文件的前半部分存儲RDB的數據,后半部分存儲的是AOF的操作命令。
9. 事務
Redis事務可以一次執行多個命令(批量命令操作),并且是一個單獨的隔離操作。事務中的所有命令都會按順序地執行。事務在執行的過程中,不會被其他客戶端發送來的命令請求所打斷。
9.1 事務操作
Redis事務主要由MULTI 、 EXEC、DISCARD、WATCH和UNWATCH這些基礎命令構成。
1)MULTI
語法:MULTI
用于標記事務的開始,后續客戶端執行的命令都將被存入一個命令隊列,直到執行EXEC時,這些命令才會被執行。
2)EXEC
語法:EXEC
執行命令隊列中的所有命令,但如果在啟用一個事務之前執行了WATCH命令,那么只有當WATCH所監控的keys沒有被修改的前提下,EXEC命令才能執行事務隊列中的所有命令,并返回所有命令的執行結果,否則EXEC將放棄當前事務中的所有命令。
3)DISCARD
語法:DISCARD
取消事務隊列中的所有命令,并將當前連接的狀態恢復為非事務狀態。如果WATCH命令被使用,會自動執行UNWATCH取消監視的所有keys。
4)WATCH
語法:WATCH key[key…]
WATCH命令類似于關系型數據庫的樂觀鎖,可以在啟用事務之前監視某些keys的變化。在MULTI命令執行之前,可以指定需要監視的keys,在執行EXEC之前,如果被監控的keys發生修改,EXEC將放棄執行該隊列中的所有指令。并且WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之后的事務就不會執行。
首先打開一個客戶端,并使用watch命令監視user:1001的key,接著使用multi啟用事務。
然后打開第二個客戶端,并修改key為user:1001的value為user01。
最后回到第一個客戶端再次對key為user:1001的value修改為user001,并執行exec命令。由于user:1001這個key被第一個客戶端所監視,而這個key在啟用事務前被第二個客戶端修改了,因此當第一個客戶端啟用事務后再對其進行修改時這是無效的,Redis將放棄隊列中的所有指令,返回了(nil)。
5)UNWATCH
語法:WATCH key[key…]
取消當前事務中指定監控的keys。如果執行了EXEC或DISCARD命令,則無需再手工執行該命令了,因為在此之后UNWATCH命令會自動執行,事務中所有的keys都將自動取消監控。
9.2 原子性
在關系型數據庫中的原子性代表一系列不可分割的操作,要么全部執行成功,要么全部不執行。如果執行過程中產生了錯誤或者異常,那么事務將會自動回滾。而在Redis的事務中是否具備原子性呢?我們看看以下兩種情況,并得出相關的結論。
錯誤指令
在使用multi命令開啟事務之后,然后輸入一些命令,其中包含一個錯誤的命令。
從結果來看似乎有點符合我們對事務的理解。但仔細想想,這只是在輸入命令的時候產生語法的錯誤,Redis對其進行了校驗,報錯之后Redis就放棄了這個事務。因此得出的結論是:Redis在啟用事務輸入操作命令時是原子操作,它會對命任何一個命令進行語法檢查,當輸入有誤時,Redis會清空隊列并放棄事務。
運行時錯誤
如果輸入的命令都正確,而在執行這些命令時產生了錯誤,Redis是否會取消所有命令并放棄事務呢?看下面的例子。
當執行到第二條命令時產生了錯誤(用戶名不是一個整型數值,并不能自增),但是前面和后面的命令都執行成功。并不會因為執行了一個錯誤的命令而回退所有已經執行成功的命令并放棄整個事務。因此得出的結論是:Redis在執行命令隊列時并不是原子性的,通俗點說就是Redis本身并不支持事務的回滾機制。
10. Java客戶端
10.1 使用Lettuce
Lettuce是Redis的一個java客戶端,同類的產品還有Jedis。但在多線程的環境下,使用Jedis是非線程安全的。而Lettuce的連接對象是基于Netty,在多線程并發訪問時是線程安全的,并且單個連接對象可以滿足多線程并發訪問的要求。Lettuce還支持同步、異步、反應式、發布/訂閱等多種通信方式。
添加依賴
<dependency><groupId>io.lettuce</groupId><artifactId>lettuce-core</artifactId><version>5.2.2.RELEASE</version>
</dependency>
編寫LettuceUtils:
public class LettuceUtils {private static StatefulRedisConnection connection;static {RedisURI redisURI = RedisURI.Builder.redis("localhost").withPort(6379).withPassword("123456").withDatabase(0).withTimeout(Duration.ofSeconds(5)).build();connection = RedisClient.create(redisURI).connect();}/*** Sync* @return*/public static RedisCommands getCommands() {return connection.sync();}/*** Async* @return*/public static RedisAsyncCommands getAsyncCommands(){return connection.async();}/*** Reactive* @return*/public static RedisReactiveCommands getReactiveCommands(){return connection.reactive();}
}
示例:
@Test
public void testGetAndSet(){RedisCommands<String, String> commands = LettuceUtils.getCommands();commands.set("key", "Hello, Redis!");String value = commands.get("key");System.out.println(value);
}
參考:https://www.baeldung.com/java-redis-lettuce
11 整合Spring Boot
添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId>
</dependency>
yml配置
spring:# redis配置redis:# 指定使用第幾個庫,默認是0database: 0# 主機地址host: 127.0.0.1# 端口port: 6379# 認證密碼(如果在redis.conf中配置了密碼)password: wangl# 連接超時時間timeout: 2000
# 連接池配置
# lettuce:
# pool:
# # 最大連接數
# max-active: 10
# # 最大空閑連接
# max-idle: 8
# # 最小空閑連接
# min-idle: 5
# # 建立連接最大等待時間
# max-wait: 2000
# # 每間隔多少毫秒運行一次空閑連接回收
# time-between-eviction-runs: 60000
由于Lettuce的連接是基于Netty的,一個連接對象(StatefulRedisConnection)就可以滿足多線程環境下的并發訪問,許多場景是不需要配置連接池的。如果在特定場景下需要用到連接池,那么在yml進行添加lettuce pool的配置,需要依賴commons-pool2。
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
10.1 使用RedisTemplate
我們可以在類中直接注入一個StringRedisTemplate,key和value都為String類型,它繼承自RedisTemplate。
@Autowired
private StringRedisTemplate stringRedisTemplate;
示例:
@Test
void testForValue() {//添加stringRedisTemplate.opsForValue().set("user:1001", "user1");//依據key獲取valueString name = stringRedisTemplate.opsForValue().get("user:1001");System.out.println(name);//刪除stringRedisTemplate.delete("user:1001");
}
Spring提供了多種Operations接口來對不同的數據結構進行操作,以下是常見的接口:
接口 | 獲取方式 | 說明 |
---|---|---|
ValueOperations | redisTemplate.opsForValue() | 操作字符串 |
ListOperations | redisTemplate.opsForList() | 操作List |
SetOperations | redisTemplate.opsForSet() | 操作Set |
ZSetOperations | redisTemplate.opsForZSet() | 操作ZSet |
HashOperations | redisTemplate.opsForHash() | 操作Hash |
示例:
stringRedisTemplate.opsForList().leftPush("list","user1");
stringRedisTemplate.opsForList().rightPush("list","user2");
參考:https://www.jianshu.com/p/7bf5dc61ca06
自定義序列化器
StringRedisTemplate操作的key和value都是字符串,如果我們需要存儲其他類型的數據,那么可以自定義key和value的序列化器來裝配一個RedisTemplate。
編寫配置類
@Configuration
public class RedisConfig {/*** 自定義RedisTemplate,指定key和value的序列化器* @param connectionFactory* @return*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){//創建RedisTemplate實例RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();//使用StringRedisSerializer作為key的序列化器StringRedisSerializer keySerializer = new StringRedisSerializer();redisTemplate.setKeySerializer(keySerializer);redisTemplate.setHashKeySerializer(keySerializer);//使用Jackson2JsonRedisSerializer作為value的序列化器GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();redisTemplate.setValueSerializer(valueSerializer);redisTemplate.setHashValueSerializer(valueSerializer);redisTemplate.setConnectionFactory(connectionFactory);return redisTemplate;}}
spring提供了多種序列化器,以下列出常見的Serializer:
序列化器 | 說明 |
---|---|
StringRedisSerializer | 簡單的字符串序列化 |
GenericToStringSerializer | 可以將任何對象泛化為字符串并序列化 |
GenericJackson2JsonRedisSerializer | 使用Jackson將對象序列化為JSON字符串 |
JdkSerializationRedisSerializer | 使用JDK提供的序列化功能 |
示例:
//注入redisTemplate
@Autowired
private RedisTemplate<String, Object> redisTemplate@Test
void test2(){Users user = new Users();user.setId(1001);user.setUserName("user1");user.setAge(21);redisTemplate.opsForValue().set("user:1001", user);Users u = (Users) redisTemplate.opsForValue().get("user:1001");log.info(u.getUserName());
}
10.2 使用緩存Cache
除了使用RedisTemplate,還可以使用Spring提供的Spring Cache來操作Redis。Spring Cache是一個非常靈活的緩存解決方案,對眾多的緩存框架進行了統一的封裝,當底層使用不同的緩存框架時,由對應的緩存管理器來進行管理。Spring 3.1內置了五個緩存管理器:
- SimpleCacheManager
- NoOpCacheManager
- ConcrrentMapCacheManager
- CompositeCacheManager
- EhCacheCacheManager
Spring 3.2引入了另外的一個緩存管理器,這個緩存管理器可以在基于JCache(JSR-107)的緩存提供商之中。除了核心的Spring框架,Spring data又提供了兩個緩存管理器。
- RedisCacheManager 來自于Spring-data-redis項目
- GemfireCacheManager 來自于Spring-data-GemFire項目
所以我們選擇緩存管理器時候,取決于使用底層緩存供應商
10.2.1 啟用緩存
在配置類上添加@EnableCaching
@Configuration
@EnableCaching
public class RedisConfig {...
}
10.2.2 配置緩存管理器
這里底層使用的Redis作為緩存技術,因此需要裝配RedisCacheManager這個緩存管理器。
@Configuration
@EnableCaching
public class RedisConfig {/*** 裝配RedisCacheManager,這里初始化了cache1和cache2兩個緩存,并存入Map中,* 后續在使用時可以指定操作哪一個緩存。* @param redisConnectionFactory* @return*/@Beanpublic RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {Map<String, RedisCacheConfiguration> map = new HashMap<>(2);map.put("cache1", initRedisCacheConfiguration(1800L));map.put("cache2", initRedisCacheConfiguration(3600L));RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).withInitialCacheConfigurations(map).build();return cacheManager;}/*** Redis緩存配置,配置相關的key和value序列化器以及緩存過期時間* serializeKeysWith()方法用于設置key的序列化器* serializeValuesWith()方法用于設置value的序列化器* entryTtl()方法用于設置過期時間* @param ttl* @return*/private RedisCacheConfiguration initRedisCacheConfiguration(Long ttl) {RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();return cacheConfiguration//設置key的序列化器.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))//設置value的序列化器.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))//設置緩存過期時間.entryTtl(Duration.ofSeconds(ttl));}
}
我們可以創建多個緩存實例(如:cache1、cache2),并對這些緩存做相關的配置(比如設置緩存key和value的序列化器以及指定緩存的活期時間等等)。在使用時可以指定要將數據放入哪個緩存實例中,最后將這些緩存實例納入緩存管理器中管理。
10.2.3 緩存注解
Spring 3.1 引入了基于注釋的緩存技術,通過少量的配置 annotation 注釋即可使得既有代碼支持緩存。
@Cacheable
該注解標記在一個方法上時表示該方法是支持緩存的,當標記在一個類上時則表示該類所有的方法都是支持緩存的。對于一個支持緩存的方法,Spring會在其標注的方法調用后將其返回值緩存起來,以保證下次利用同樣的參數來執行該方法時可以直接從緩存中獲取結果,而不需要再次執行該方法。
示例:
@Service
public class UserInfoServiceImpl {@Autowiredprivate UserInfoDaoImpl dao;//這里指定的緩存名為cache1,與配置中的名字一樣,緩存的過期時間為1800//如果指定的不是配置中的名字,就會使用默認的配置,比如過期時間是-1(永不過期)@Cacheable(value = "cache1",key = "#id")public UserInfo getUser(Integer id) {UserInfo result = dao.getById(id);return result;}}
在業務類方法上使用@Cacheable注解,當執行完此方法后會將dao返回的結果保存在緩存中。如果下次傳入相同id查詢時會從緩存獲取,不會再次調用getUser方法。
@Cacheable主要屬性:
參數 | 說明 | 示例 |
---|---|---|
value | 緩存的名稱,在配置類中定義,必須指定至少一個 | @Cacheable(value=”cache1”) 或者 @Cacheable(value={”cache1”,”cache2”} |
key | 緩存的key,可以為空,如果指定就要按照SpEL表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 | @Cacheable(value=”cache1”,key=”#userName”) |
condition | 緩存的條件,可以為空,使用SpEL編寫,返回true或者false,只有為true才進行緩存 | @Cacheable(value=”cache1”, key=”#userName” condition=”#userName.length()>2”) |
unless | 用來排除緩存,返回true的時候不放在緩存中 | @Cacheable(value=“cache1”, key=“#id”, unless=“#result==null”) 表示方法返回值為null時不放置在緩存中 |
key的生成策略:
spring緩存中的key是按照一定規則來生成的,默認情況下是@Cacheable注解中value屬性的值加上"::" 再加上key屬性的值來生成key。例如:
@Cacheable(value="abc",key="#id")
public UserInfo getById(Integer id)
如果調用getById方法時傳入的參數為100,那么最終生成的key就是abc::100
自定義key的生成策略:
我們也可以在裝配RedisCacheConfiguration時設置key生成策略,例如:
RedisCacheConfiguration initRedisCacheConfiguration(Long ttl) {RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();return cacheConfiguration//設置key的序列化器.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))//設置value的序列化器.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))//設置緩存過期時間.entryTtl(Duration.ofSeconds(ttl));設置緩存key的前綴,cacheName的值由@Cacheable注解的value屬性決定.computePrefixWith(cacheName -> "demo".concat(":").concat(cacheName).concat(":"));}
}
上面調用RedisCacheConfiguration的computePrefixWith方法來設置key的前綴,此時再去調用getById方法時,最終生成的key為demo:abc:100
。
key表達式:
@Cacheable的key屬性支持SpringEL表達式,這里的EL表達式可以使用方法參數及它們對應的屬性。使用方法參數時我們可以直接使用“#參數名”或者“#p加上參數下標”。例如:
@Cacheable(value="users", key="#id")
public User getUser(Integer id) {return dao.getUserById(id);
}@Cacheable(value="users", key="#p0")
public User getUser(Integer id) {return dao.getUserById(id);
}@Cacheable(value="users", key="#user.id")
public User getUser(User user) {return dao.getUserById(user.getId());
}@Cacheable(value="users", key="#p0.id")
public User getUser(User user) {return dao.getUserById(user.getId());
}
除了上述使用方法參數作為key之外,Spring還為我們提供了一個root對象可以用來生成key。通過該root對象我們可以獲取到以下信息。
屬性名稱 | 說明 | 示例 |
---|---|---|
methodName | 當前方法名 | #root.methodName |
method | 當前方法 | #root.method.name |
target | 當前被調用的對象 | #root.target |
targetClass | 當前被調用對象的class | #root.targetClass |
args | 當前方法參數組成的數組 | #root.args[0] |
caches | 當前被調用的方法使用的Cache名稱 | #root.caches[0].name |
示例:
@Cacheable(value="users", key="#root.methodName")
public User getUser(Integer id) {return dao.getUserById(id);
}
此時將使用當前方法名作為緩存的key。
@CachePut
@CachePut標注的方法在執行前不會去檢查緩存中是否存有緩存的數據,而是每次都會執行該方法,并將執行結果再次保存到指定的緩存中,相當于覆蓋緩存。
示例:
@CachePut(value = "cache1",key = "#user.id")
public UserInfo insert(User user) {dao.insert(userInfo);return user;
}
@Cacheable主要屬性:
屬性 | 說明 |
---|---|
value | 緩存的名稱,在配置類中定義,必須指定至少一個 |
key | 緩存的key,可以為空,如果指定就要按照SpEL表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 |
condition | 緩存的條件,可以為空,使用SpEL編寫,返回true或者false,只有為true才進行緩存 |
@CachEvict
該注解根據一定的條件對緩存進行清空
示例:
@CacheEvict(value="cache1", key="#id")
public void deleteById(Integer id) {dao.deleteById(id);
}
@Cacheable主要屬性:
屬性 | 說明 |
---|---|
value | 緩存的名稱,在配置類中定義,必須指定至少一個 |
key | 緩存的key,可以為空,如果指定就要按照SpEL表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 |
condition | 緩存的條件,可以為空,使用SpEL編寫,返回true或者false,只有為true才進行緩存 |
allEntries | 是否清空所有緩存內容,缺省為 false,如果指定為 true,則方法調用后將立即清空所有緩存 |
beforeInvocation | 是否在方法執行前就清空,缺省為 false,如果指定為 true,則在方法還沒有執行的時候就清空緩存,缺省情況下,如果方法執行拋出異常,則不會清空緩存 |