學習5種基本數據結構的指令和java api。以及分佈式鎖,位圖,HyperLogLog,布隆過濾器(BloomFliter)等多種高級數據結構的指令和java api,以及具體應用場景。
數據結構
Redis五種數據結構,分別爲string,list,hash,set,zset。
string
key,value數據結構。
存儲結構類似java的數組。
指令
- set key value
- get key
- exists key
- del key
- mset key1 value1 key2 value2 …
- mget key1 key2 …
- expire key expire_time_sec :設置過期時間
- setex key expire_time_sec value : 創建時設置過期時間
- setnx key value : 不存在key則創建,set會直接覆蓋
測試代碼
private static void testString() throws InterruptedException {
String key = "string";
// 1. 先刪除指定的key
jedis.del(key);
// 2. 添加字符串,過期時間5s,存在則不添加
jedis.set(key, "value", new SetParams().ex(5).nx());
// 3. 查詢符合模式的所有key
// h?llo will match hello hallo hhllo
// h*llo will match hllo heeeello
// h[ae]llo will match hello and hallo, but not hillo
String pattern = "str*";
System.out.println("keys, PATTERN : {" + pattern + "}, RETURN : {" + jedis.keys(pattern) + "}");
// 4. 查詢過期時間
System.out.println("KEY : {" + key + "}, expire time : {" + jedis.ttl(key) + "}s");
// 5. 移除過期時間
jedis.persist(key);
System.out.println("persist KEY , expire time : {" + jedis.ttl(key) + "}s");
// 6. 隨機訪問
String randomKey = jedis.randomKey();
// 7. 查看類型
System.out.println("RANDOM KEY : {" + randomKey + "},TYPE : {" + jedis.type(randomKey) + "}");
}
打印如下:
keys, PATTERN : {str*}, RETURN : {[string]}
KEY : {string}, expire time : {5}s
persist KEY , expire time : {-1}s
RANDOM KEY : {hash},TYPE : {hash}
list
key, value1,value2數據結構,類似java的list。
兩種存儲結構:當數據量小時,使用壓縮列表(ziplist);數量大大時,使用快速列表(quicklist)
struct ziplist<T>{
int32 zlbytes; // 整個壓縮列表佔用字節數
int32 zltail_offset; // 最後一個元素距離壓縮列表起始位置的偏移量,用於快速定位到最後一個節點
int16 zllength; // 元素個數
T[] entries; // 元素內容列表,依次緊湊存儲
int8 zlend; // 標誌壓縮列表的結束
}
struct entry{
int<var> prevlen; // 前一個entry的字節長度
int<var> encoding; // 元素內容編碼
optional byte[] content; // 元素內容
}
struct quicklist{
quicklistNode* head;
quicklistNode* tail;
long count; // 元素總和
int nodes; // ziplist節點的個數
int compressDepth; // LZF 算法壓縮度
}
struct quicklistNode{
quicklistNode* prev;
quicklistNode* next;
ziplist* zl; // 指向壓縮列表
int32 size; // ziplist 的字節總數
int16 count; // ziplist 中的元素數量
int2 encoding; // 存儲形式2bit
}
quicklist 內部默認單個ziplist長度爲8kb,超過這個字節會另起一個ziplist。可以通過參數list-max-ziplist-size決定。
指令
- push : lpush / rpush
rpush key value1 value2 value3
- pop : lpop / rpop
lpop key
- lindex
- ltrim
- lrange
程序
private static void testList() {
// 1. 先刪除指定key
jedis.del("list");
// 2. 左添加
jedis.lpush("list", "listValue3", "listValue2", "listValue1");
// 右添加
jedis.rpush("list", "listValue4", "listValue5", "listValue6");
// 3. 取出所有元素
System.out.println(jedis.lrange("list", 0, -1));
// 4. 取出最後一個元素
System.out.println(jedis.lindex("list", -1));
// 5. 移除部分數據,保留1-3
jedis.ltrim("list", 1, 3);
System.out.println(jedis.lrange("list", 0, -1));
}
打印如下:
[listValue1, listValue2, listValue3, listValue4, listValue5, listValue6]
listValue6
[listValue2, listValue3, listValue4]
hash
key,k1,value1,k2,value2,類似java中的Map。
存儲結構類似java的HashMap,使用數組加鏈表的方式來存儲。rehash採用循環漸進的處理,兩組hashtable,將舊的值一步一步的遷移到新的(hset、hdel操作),如果客戶端沒有操作,將會通過定時器,定時執行
指令
- hset key k1 v1
- hgetAll
- hlen
- hmget
- hmset
程序
private static void testHash() {
String key = "hash";
// 1. 先刪除指定key
jedis.del(key);
Map<String, String> map = new HashMap<>();
map.put("k1", "v1");
map.put("k2", "v2");
// 2. 添加數據結構
jedis.hset(key, map);
// 3. 追加數據
jedis.hset(key, "k3", "v3");
// 4. 獲取數據和長度
System.out.println("獲取所有數據:" + jedis.hgetAll(key) + ",長度:" + jedis.hlen(key));
// 5. 批量獲取
System.out.println("批量獲取數據:" + jedis.hmget(key, "k1", "k3"));
// 6. 計數
jedis.hset(key, "k4", 1 + "");
jedis.hincrBy(key, "k4", 1);
System.out.println("獲取數據:" + jedis.hget(key, "k4"));
}
打印日誌
獲取所有數據:{k3=v3, k1=v1, k2=v2},長度:3
批量獲取數據:[v1, v3]
獲取數據:2
set
key,value1,value2,類似java的Set,value不能重複。
特殊的hash,值爲NULL,無序
指令
- sadd key value1 value2
- smembers key(無序)
- sismember key value : 查詢key是否存在value,存在則返回1,否則返回0
- scard
- spop
程序
private static void testSet() {
String key = "set";
// 1. 先刪除指定key
jedis.del(key);
// 2. 添加數據結構
jedis.sadd(key, "value1", "value2");
// 3. 獲取數據和長度
System.out.println("獲取所有數據:" + jedis.smembers(key) + ",長度:" + jedis.scard(key));
// 4. 是否存在指定value
System.out.println("是否存在數據:" + jedis.sismember(key, "value2"));
// 5. 彈出數據
System.out.println("彈出數據:" + jedis.spop(key));
// 6. 獲取數據
System.out.println("獲取數據:" + jedis.smembers(key));
}
zset
有序Set
存儲結構爲:跳躍列表
指令
- zadd
- zrange
- zrevrange : 按score逆序列出
- zcard
- zscore : 獲取指定key的分數
- zrank : 獲取指定key的排名
- zrangebyscore : 遍歷分值區域
- zrem : 刪除value
程序
private static void testZSet() {
String key = "zset";
// 1. 先刪除指定key
jedis.del(key);
// 2. 添加數據結構
jedis.zadd(key, 100, "value1");
jedis.zadd(key, 50, "value2");
jedis.zadd(key, 0, "value3");
// 3. 獲取數據
System.out.println("獲取數據:" + jedis.zrange(key, 0, 100));
// 4. 是否存在指定value
System.out.println("獲取數據:" + jedis.zrevrange(key, 0, 100));
// 5. 數據量
System.out.println("數據量:" + jedis.zcard(key));
// 6. 獲取數據
System.out.println("獲取數據:" + jedis.zscore(key, "value2"));
// 7. 獲取數據
System.out.println("刪除數據:" + jedis.zrem(key, "value2"));
System.out.println("獲取數據:" + jedis.zrange(key, 0, 100));
}
打印如下:
獲取數據:[value3, value2, value1]
獲取數據:[value1, value2, value3]
數據量:3
獲取數據:50.0
刪除數據:1
獲取數據:[value3, value1]
應用
分佈式鎖(重要)
set key value ex expire_time_sec nx (原子操作)
設置超時時間(ex),以及存在則不添加(nx)。可以設置一個隨機值作爲value,在刪除鎖時候,判斷value是否一致。
超時問題
如果函數在超時時間內無法結束,則會出現問題。解決方案:redisson插件,原理如下:
加鎖時,會有一個監聽線程,當持鎖線程還在執行則會追加超時時間。
private static RedissonClient redissonClient;
public static void main(String[] args) {
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
redissonClient = Redisson.create(config);
}
// 非公平鎖
private static void testLock() {
try {
System.out.println(Thread.currentThread().getName());
RLock rLock = redissonClient.getLock("key");
// 這一步線程阻塞
System.out.println(Thread.currentThread().getName() + ",準備加鎖。。。。。。");
rLock.lock(3, TimeUnit.SECONDS);
System.out.println(Thread.currentThread().getName() + "加鎖成功");
Thread.sleep(5000);
rLock.unlock();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
消息隊列
使用list數據結構作爲隊列,lpush爲生產消息,rpop爲消費消息。
可以使用brpop來代替rpop。爲了避免空閒連接自動斷開,所以需要捕獲異常,然後重試。
位圖(加分)
使用位圖getbit/setbit等操作對string類型進行處理。
指令
- getbit
- setbit
- bitfield key get
- bitfield key set
- bitfield key incrby
應用場景
用戶簽到記錄
程序
public static void main(String[] args) {
try {
jedis = new Jedis("localhost", 6379);
// 實現用戶的簽到記錄
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("YYYY-MM-DD");
long beginTime = simpleDateFormat.parse("2020-01-01").getTime();
// 位移,當天是當年的第幾天
Long day = (System.currentTimeMillis() - beginTime) / 1000 * 60 * 60 * 24L;
jedis.setbit("user1", day.intValue(), true);
System.out.println(jedis.getbit("user1", day.intValue()));
} catch (ParseException e) {
e.printStackTrace();
} finally {
jedis.close();
}
}
HyperLogLog(加分)
高級數據結構HyperLogLog,提供不精確的去重技術方案,標準誤差率在0.81%,佔用內存空間12KB。
指令
- pfadd
- pfcount
- pfmerge
注意:在計數比較小時,採用稀疏矩陣存儲,空間佔用很小,當計數慢慢變大、稀疏矩陣佔用空間漸漸超過了閾值時,纔會一次性轉變爲稠密矩陣,纔會佔用12KB空間。
應用場景
記錄某個連接的UV
程序
public static void main(String[] args) throws InterruptedException {
// 大致統計用戶數量
try {
jedis = new Jedis("localhost", 6379);
for (int i = 0; i < 1000; i++) {
jedis.pfadd("hyper1", "user" + i);
}
System.out.println(jedis.pfcount("hyper1"));
for (int i = 0; i < 1000; i++) {
jedis.pfadd("hyper2", "user" + (1000 + i));
}
System.out.println(jedis.pfcount("hyper2"));
jedis.pfmerge("hyper3", "hyper1", "hyper2");
System.out.println(jedis.pfcount("hyper3"));
} finally {
jedis.close();
}
}
// 打印如下
1011
997
2002
布隆過濾器(重要)
高級數據結構布隆過濾器(Bloom Filter)。提供去重作用,在空間上能節省90%以上,存在一定的誤判概率。
注意:需要安裝插件,https://github.com/RedisBloom/RedisBloom;docker可以使用鏡像redislabs/rebloom
指令
- bf.add key value
- bf.exists key value
- bf.madd key value1 value2
- bf.exists key value1 value2
- bf.reserve key error_rate(錯誤率,默認0.01) initial_size(默認100)
應用場景
數據去重
緩存穿透
程序
// 添加mavan依賴
<!-- https://mvnrepository.com/artifact/com.redislabs/jrebloom -->
<dependency>
<groupId>com.redislabs</groupId>
<artifactId>jrebloom</artifactId>
<version>2.0.0-m1</version>
</dependency>
public static void main(String[] args) {
try {
jedis = new Jedis("localhost", 6379);
// 設置布隆過濾器參數
jedis.sendCommand(io.rebloom.client.Command.RESERVE, "bf", "0.01", "5");
// 第一種場景:數據去重
// 當使用過數據,添加到redis,通過exists來判斷是否存在:0爲不存在,1爲存在
jedis.sendCommand(io.rebloom.client.Command.ADD, "bf", "user1");
jedis.sendCommand(io.rebloom.client.Command.ADD, "bf", "user2");
System.out.println(jedis.sendCommand(io.rebloom.client.Command.EXISTS, "bf", "user1"));
System.out.println(jedis.sendCommand(io.rebloom.client.Command.EXISTS, "bf", "user3"));
// 第二種場景:緩存穿透
// 將所有緩存預熱到指定key中,存在則查詢,不存在則直接返回
Set<String> keys = jedis.keys("*");
keys.forEach(key->{
jedis.sendCommand(Command.ADD, "all", key);
});
long result = (Long) jedis.sendCommand(io.rebloom.client.Command.EXISTS, "all", "user3");
if(result == 1){
System.out.println("執行緩存查詢");
}else{
System.out.println("直接返回");
}
} finally {
jedis.close();
}
}
// 日誌打印
1
0
直接返回
原理
通過錯誤率和初始大小,生成位數組的大小和hash函數數量,公式如下:
hash函數數量 = 0.7 * (位數組的大小 / 初始大小)
錯誤率 = 0.6185^(位數組的大小 / 初始大小)
布隆計算器:https://krisives.github.io/bloom-calculator
通過計算器可知,100個初始大小,錯誤率在1%的情況下,有7個hash函數和959長度的位數組,那麼具體的算法如下:
- 7個hash函數,分別爲h1,h2,h3,h4,h5,h6,h7
- 當添加一個value時
- 第一個位置:location1 = h1(value) % 959,將該位置設置爲1,(setbit key location1 1);
- 第二個位置:location2 = h2(value) % 959,將該位置設置爲1,(setbit key location2 1);
- 重複以上步驟,直到獲取第七個位置
- 在判斷是否存在指定value,也是類似的算法
- 第一個位置:location1 = h1(value) % 959,獲取位置1的值,(getbit key location1);如果不爲1則直接返回不存在
- 只有當七個位置都爲1的情況下,才表示該value是存在的
錯誤率的原因是因爲hash時,可能存在hash衝突
GeoHash(地理位置算法)
指令
- geoadd key 經度 緯度 value1
- 計算兩個元素之間的距離(指定單位):geodist key value1 value2 m/km/ml/ft
- 獲取指定元素的經緯度:geopos key value1
- 獲取指定元素的hash:geohash key value1
- 查詢指定元素相關的元素:georadiusbymember key value1 x m/km/ml/ft [withcoord/withdist\withhash] count y asc/desc
- 查詢指定經緯度相關的元素:georadius key 經度 緯度 x m/km/ml/ft [withcoord/withdist\withhash] count y asc/desc
withcoord ;withdist:顯示距離;withhash:顯示hash
程序
private static Jedis jedis;
public static void main(String[] args) {
try {
Jedis jedis = new Jedis("localhost", 6379);
String key = "company";
jedis.geoadd(key, 116.48105, 39.996794, "掘金");
Map<String, GeoCoordinate> map = new HashMap<>(3);
map.put("掌閱", new GeoCoordinate(116.514203, 39.905409));
map.put("美團", new GeoCoordinate(116.489033, 40.007669));
map.put("京東", new GeoCoordinate(116.562108, 39.9787602));
jedis.geoadd(key, map);
System.out.println("美團、京東距離:"+ jedis.geodist(key, "美團","京東", GeoUnit.M) + "米");
// 範圍20公里內的三家公司
List<GeoRadiusResponse> list = jedis.georadiusByMember(key, "京東", 20, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().count(3));
list.forEach(geoRadiusResponse -> {
System.out.println(geoRadiusResponse.getMemberByString() + geoRadiusResponse.getDistance());
});
List<GeoRadiusResponse> list2 = jedis.georadius(key, 116.562108, 39.9787602, 20, GeoUnit.KM, GeoRadiusParam.geoRadiusParam().withDist().count(3));
list2.forEach(geoRadiusResponse -> {
System.out.println(geoRadiusResponse.getMemberByString() + geoRadiusResponse.getDistance());
});
} finally {
jedis.close();
}
}
// 日誌打印
美團、京東距離:7008.1463米
京東0.0
美團7.0081
掘金7.1929
京東2.0E-4
美團7.0082
掘金7.193