SpringBoot-Redis 入門
Redis 的數(shù)據(jù)類型
String 字符串
- string 是 redis 最基本的類型,一個 key 對應(yīng)一個 value。
- string 類型是二進(jìn)制安全的。意思是 redis 的 string 可以包含任何數(shù)據(jù)。比如 jpg 圖片或者序列化的對象 。
- string 類型是 Redis 最基本的數(shù)據(jù)類型,一個鍵最大能存儲 512MB。
- String 類型的操作參考
鏈表
- redis 列表是簡單的字符串列表,排序為插入的順序。列表的最大長度為 2^32-1。
- redis 的列表是使用鏈表實現(xiàn)的,這意味著,即使列表中有上百萬個元素,增加一個元素到列表的頭部或尾部的操作都是在常量的時間完成。
- 可以用列表獲取最新的內(nèi)容(像帖子,微博等),用 ltrim 很容易就會獲取最新的內(nèi)容,并移除舊的內(nèi)容。
- 用列表可以實現(xiàn)生產(chǎn)者消費者模式,生產(chǎn)者調(diào)用 lpush 添加項到列表中,消費者調(diào)用 rpop 從列表中提取,如果沒有元素,則輪詢?nèi)カ@取,或者使用 brpop 等待生產(chǎn)者添加項到列表中。
- List 類型的操作參考
集合
- redis 集合是無序的字符串集合,集合中的值是唯一的,無序的。可以對集合執(zhí)行很多操作,例如,測試元素是否存在,對多個集合執(zhí)行交集、并集和差集等等。
- 我們通常可以用集合存儲一些無關(guān)順序的,表達(dá)對象間關(guān)系的數(shù)據(jù),例如用戶的角色,可以用 sismember 很容易就判斷用戶是否擁有某個角色。
- 在一些用到隨機(jī)值的場合是非常適合的,可以用 srandmember/spop 獲取/彈出一個隨機(jī)元素。
同時,使用@EnableCaching 開啟聲明式緩存支持,這樣就可以使用基于注解的緩存技術(shù)。注解緩存是一個對緩存使用的抽象,通過在代碼中添加下面的一些注解,達(dá)到緩存的效果。 - Set 類型的操作參考
ZSet 有序集合
- 有序集合由唯一的,不重復(fù)的字符串元素組成。有序集合中的每個元素都關(guān)聯(lián)了一個浮點值,稱為分?jǐn)?shù)??梢园延行蚩闯?hash 和集合的混合體,分?jǐn)?shù)即為 hash 的 key。
- 有序集合中的元素是按序存儲的,不是請求時才排序的。
- ZSet 類型的操作類型
Hash-哈希
- redis 的哈希值是字符串字段和字符串之間的映射,是表示對象的完美數(shù)據(jù)類型。
- 哈希中的字段數(shù)量沒有限制,所以可以在你的應(yīng)用程序以不同的方式來使用哈希。
- Hash 類型的操作參考
關(guān)于 key 的設(shè)計
key 的存活時間:
無論什么時候,只要有可能就利用 key 超時的優(yōu)勢。一個很好的例子就是儲存一些諸如臨時認(rèn)證 key 之類的東西。當(dāng)你去查找一個授權(quán) key 時——以 OAUTH 為例——通常會得到一個超時時間。
這樣在設(shè)置 key 的時候,設(shè)成同樣的超時時間,Redis 就會自動為你清除。
關(guān)系型數(shù)據(jù)庫的 redis
- 把表名轉(zhuǎn)換為 key 前綴 如, tag:
- 第 2 段放置用于區(qū)分區(qū) key 的字段--對應(yīng) mysql 中的主鍵的列名,如 userid
- 第 3 段放置主鍵值,如 2,3,4...., a , b ,c
- 第 4 段,寫要存儲的列名
例:user:userid:9:username
RedisTemplate 常用操作集合
| 方法 | Redis 類型 | 備注 |
|---|---|---|
| opsForValue() | String | 對 redis 字符串類型數(shù)據(jù)操作 |
| opsForList() | List | 對鏈表類型的數(shù)據(jù)操作 |
| opsForHash() | Hash | 對 hash 類型的數(shù)據(jù)操作 |
| opsForSet() | Set | 對無序集合類型的數(shù)據(jù)操作 |
| opsForZSet() | ZSet | 對有序集合類型的數(shù)據(jù)操作 |
Serializer
目前已經(jīng)支持的序列化策略:
- JdkSerializationRedisSerializer:POJO 對象的存取場景,使用 JDK 本身序列化機(jī)制,將 pojo 類通過 ObjectInputStream/ObjectOutputStream 進(jìn)行序列化操作,最終 redis-server 中將存儲字節(jié)序列。是目前最常用的序列化策略
- StringRedisSerializer :Key 或者 value 為字符串的場景,根據(jù)指定的 charset 對數(shù)據(jù)的字節(jié)序列編碼成 string,是 “new String(bytes, charset)” 和 “string.getBytes(charset)” 的直接封裝。是最輕量級和高效的策略。
- JacksonJsonRedisSerializer:jackson-json 工具提供了 javabean 與 json 之間的轉(zhuǎn)換能力,可以將 pojo 實例序列化成 json 格式存儲在 redis 中,也可以將 json 格式的數(shù)據(jù)轉(zhuǎn)換成 pojo 實例。因為 jackson 工具在序列化和反序列化時,需要明確指定 Class 類型,因此此策略封裝起來稍微復(fù)雜。【需要 jackson-mapper-asl 工具支持】
- OxmSerializer :提供了將 javabean 與 xml 之間的轉(zhuǎn)換能力,目前可用的三方支持包括 jaxb,apache-xmlbeans;redis 存儲的數(shù)據(jù)將是 xml 工具。不過使用此策略,編程將會有些難度,而且效率最低;不建議使用。【需要 spring-oxm 模塊的支持】
其中 JdkSerializationRedisSerializer 和 StringRedisSerializer 是最基礎(chǔ)的序列化策略,其中 “JacksonJsonRedisSerializer” 與 “OxmSerializer” 都是基于 String 存儲,因此它們是較為“高級”的序列化 (最終還是使用 string 解析以及構(gòu)建 java 對象)。
如果你的數(shù)據(jù)需要被第三方工具解析,那么數(shù)據(jù)應(yīng)該使用 StringRedisSerializer 而不是 JdkSerializationRedisSerializer。
Redis Pipline
通過 RedisTemplete 實現(xiàn) pipline 可以參考如下代碼:
public List<Object> queryAll() {
return redisTemplate.executePipelined((RedisConnection redisConnection) -> {
RedisSerializer<String> stringSerializer = redisTemplate.getStringSerializer();
Set<String> keys = redisTemplate.keys("*");
if (Objects.nonNull(keys)) {
for (String key : keys) {
redisConnection.get(stringSerializer.serialize(key));
}
}
return null;
});
}
需要注意的是 redisTemplate.executePipelined() 里面的方法返回值必須為 null.
原因是該方法的源碼如下:
public List<Object> executePipelined(final RedisCallback<?> action) {
return executePipelined(action, valueSerializer);
}
public List<Object> executePipelined(final RedisCallback<?> action, final RedisSerializer<?> resultSerializer) {
return execute(new RedisCallback<List<Object>>() {
public List<Object> doInRedis(RedisConnection connection) throws DataAccessException {
connection.openPipeline();
boolean pipelinedClosed = false;
try {
Object result = action.doInRedis(connection);
if (result != null) {
throw new InvalidDataAccessApiUsageException(
"Callback cannot return a non-null value as it gets overwritten by the pipeline");
}
List<Object> closePipeline = connection.closePipeline();
pipelinedClosed = true;
return deserializeMixedResults(closePipeline, resultSerializer, resultSerializer, resultSerializer);
} finally {
if (!pipelinedClosed) {
connection.closePipeline();
}
}
}
});
}
在代碼段中有如下的判斷:
Object result = action.doInRedis(connection);
if (result != null) {
throw new InvalidDataAccessApiUsageException(
"Callback cannot return a non-null value as it gets overwritten by the pipeline");
}
因此如果所傳入的方法如果不為空,則會拋出異常,導(dǎo)致程序運行失敗。
注意:
- doInRedis 中的 redis 操作不會立刻執(zhí)行
- 所有 redis 操作會在 connection.closePipeline() 之后一并提交到 redis 并執(zhí)行,這是 pipeline 方式的優(yōu)勢
- 所有操作的執(zhí)行結(jié)果為 executePipelined() 的返回值
RedisTemplete 執(zhí)行 lua 腳本
Redis 命令行運行 Lua 腳本
假定我們有如下 lua 腳本:
--獲取KEY
local key1 = KEYS[1]
local key2 = KEYS[2]
-- 獲取ARGV[1],這里對應(yīng)到應(yīng)用端是一個List<Map>.
-- 注意,這里接收到是的字符串,所以需要用csjon庫解碼成table類型
local receive_arg_json = cjson.decode(ARGV[1])
--返回的變量
local result = {}
--打印日志到reids
--注意,這里的打印日志級別,需要和redis.conf配置文件中的日志設(shè)置級別一致才行
redis.log(redis.LOG_DEBUG,key1)
redis.log(redis.LOG_DEBUG,key2)
redis.log(redis.LOG_DEBUG, ARGV[1],#ARGV[1])
--獲取ARGV內(nèi)的參數(shù)并打印
local expire = receive_arg_json.expire
local times = receive_arg_json.times
redis.log(redis.LOG_DEBUG,tostring(times))
redis.log(redis.LOG_DEBUG,tostring(expire))
--往redis設(shè)置值
redis.call("set",key1,times)
redis.call("incr",key2)
redis.call("expire",key2,expire)
--用一個臨時變量來存放json,json是要放入要返回的數(shù)組中的
local jsonRedisTemp={}
jsonRedisTemp[key1] = redis.call("get",key1)
jsonRedisTemp[key2] = redis.call("get", key2)
jsonRedisTemp["ttl"] = redis.call("ttl",key2)
redis.log(redis.LOG_DEBUG, cjson.encode(jsonRedisTemp))
result[1] = cjson.encode(jsonRedisTemp) --springboot redistemplate接收的是List,如果返回的數(shù)組內(nèi)容是json對象,需要將json對象轉(zhuǎn)成字符串,客戶端才能接收
result[2] = ARGV[1] --將源參數(shù)內(nèi)容一起返回
redis.log(redis.LOG_DEBUG,cjson.encode(result)) --打印返回的數(shù)組結(jié)果,這里返回需要以字符返回
return result
我們可以使用如下命令行查看執(zhí)行結(jié)果:
其基本命令結(jié)構(gòu)如下:
redis-cli [--ldb] --eval script [numkeys] key [key ...] , arg [arg ...]
- --eval:告訴redis客戶端去加載Lua腳本,后面跟著的就是 lua 腳本的路徑
- --ldb :進(jìn)行命令調(diào)試的必要參數(shù)
- numkeys:指定后續(xù)參數(shù)有幾個key。可省略
-
key [key ...]:是要操作的鍵,可以指定多個,在lua腳本中通過
KEYS[1],KEYS[2]獲取 -
arg [arg ...],參數(shù),在lua腳本中通過
ARGV[1],ARGV[2]獲取。
注意: KEYS和ARGV中間的 ',' 兩邊的空格,不能省略
針對本例中的 Lua 腳本其對應(yīng)的命令行如下:
bin/redis-cli -h localhost -p 7379 -a zcvbnm --ldb --eval script/LimitLoadTimes.lua count rate.limiting:127.0.0.1 , "{\"expire\":\"10000\",\"times\":\"10\"}"
其他的一些參數(shù)
- -h 修改后的ip -a 修改后的密碼 -p 修改后的端口號
結(jié)果輸出為:
[root@VM_0_12_centos redis-4.0.8]# bin/redis-cli -h localhost -p 7379 -a zcvbnm --ldb --eval script/LimitLoadTimes.lua count rate.limiting:127.0.0.1 , "{\"expire\":\"10000\",\"times\":\"10\"}"
Lua debugging session started, please use:
quit -- End the session.
restart -- Restart the script in debug mode again.
help -- Show Lua script debugging commands.
* Stopped at 1, stop reason = step over
-> 1 local key1 = KEYS[1]
lua debugger> continue
1) "{\"rate.limiting:127.0.0.1\":\"1\",\"count\":\"10\",\"ttl\":10000}"
2) "{\"expire\":\"10000\",\"times\":\"10\"}"
使用 Java 運行 Lua 腳本
實現(xiàn)代碼如下:
package cn.sjsdfg.redis.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by Joe on 2019/5/8.
*/
@Service
public class LuaScriptService {
@Autowired
@Qualifier("customRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
private DefaultRedisScript<List> getRedisScript;
@PostConstruct
public void init(){
getRedisScript = new DefaultRedisScript<List>();
getRedisScript.setResultType(List.class);
getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/LimitLoadTimes.lua")));
}
public void redisAddScriptExec(){
/**
* List設(shè)置lua的KEYS
*/
List<String> keyList = new ArrayList<>();
keyList.add("count");
keyList.add("rate.limiting:127.0.0.1");
/**
* 用Mpa設(shè)置Lua的ARGV[1]
*/
Map<String,Object> argvMap = new HashMap<String,Object>();
argvMap.put("expire", 10000);
argvMap.put("times", 10);
/**
* 調(diào)用腳本并執(zhí)行
*/
List result = redisTemplate.execute(getRedisScript, keyList, argvMap);
System.out.println(result);
}
}
測試代碼在 cn.sjsdfg.redis.service.LuaScriptServiceTest#testRedisAddScriptExec,其輸出為:
[{rate.limiting:127.0.0.1=3, count=10, ttl=10000}, {times=10, expire=10000}]
與前面直接執(zhí)行 lua 腳本的輸出結(jié)果一致。
注意
- Lua腳本可以在redis單機(jī)模式、主從模式、Sentinel集群模式下正常使用,但是無法在分片集群模式下使用。(腳本操作的key可能不在同一個分片)
- Lua腳本中盡量避免使用循環(huán)操作(可能引發(fā)死循環(huán)問題),盡量避免長時間運行。
- redis在執(zhí)行l(wèi)ua腳本時,默認(rèn)最長運行時間時5秒,當(dāng)腳本運行時間超過這一限制后,Redis將開始接受其他命令但不會執(zhí)行(以確保腳本的原子性,因為此時腳本并沒有被終止),而是會返回“BUSY”錯誤。
spring-data-redis 和 jedis 版本對應(yīng)收集總結(jié)
如果不使用對飲版本的 Jedis,在項目構(gòu)建的時候必定會出現(xiàn) java.lang.NoClassFoundException。
Jedis 代碼重構(gòu)變革很大
| spring-data-redis 版本 | jedis 版本 | 備注 |
|---|---|---|
| 1.5.2.RELEASE | 2.7.3 | |
| 1.6.0.RELEASE | 2.7.2 2.7.3 | |
| 1.6.2.RELEASE | 2.8.0 | |
| 1.8.1.RELEASE | 2.9.0 | |
| 1.8.4.RELEASE | 2.9.0 | |
| 2.1.x.RELEASE | 2.9.0 |
參考資料
- Spring Data Redis 可查詢 feature 演進(jìn)和版本對應(yīng)關(guān)系
- spring-data-keyvalue-examples
- Spring Data Redis 簡介以及項目 Demo,RedisTemplate 和 Serializer 詳解
- redisTemplate 常用集合使用說明 (一)
- springboot 之使用 redistemplate 優(yōu)雅地操作 redis
連接 Redis 工具
- https://github.com/necan/RedisDesktopManager-Windows 提供 RedisManager 的開源編譯版本
- https://github.com/onewe/RedisDesktopManager-Mac RedisManager 軟件的 Mac 版本
- AnotherRedisDesktopManager:A faster, better and more stable redis desktop manager, compatible with Linux, windows, mac. (國產(chǎn))