1 Redis高級部分
安裝redis6.x
1.1 redis傳統(tǒng)五大基本類型的落地應(yīng)用
官網(wǎng)命令大全網(wǎng)址:http://www.redis.cn/commands.html
命令不區(qū)分大小寫,而key是區(qū)分大小寫的
help @類型名詞
- 8大類型
- String(字符類型)
- Hash(散列類型)
- List(列表類型)
- Set(集合類型)
- SortedSet(有序集合類型,簡稱zset)
- Bitmap(位圖)
- HyperLogLog(統(tǒng)計)
- GEO(地理)
1.1.1 String
set key value
get key
同時設(shè)置/獲取多個鍵值
MSET key value [key value ....]
MGET key [key ....]
數(shù)值增減
遞增數(shù)字 INCR key
增加指定的整數(shù) INCRBY key increment
遞減數(shù)值 DECR key
減少指定的整數(shù) DECRBY key decrement
獲取字符串長度
STRLEN key
分布式鎖
setnx key value
set key value [EX seconds] [PX milliseconds] [NX|XX]
應(yīng)用場景
商品編號、訂單號采用INCR命令生成
是否喜歡的文章
- EX:key在多少秒之后過期
- PX:key在多少毫秒之后過期
- NX:當(dāng)key不存在的時候,才創(chuàng)建key,效果等同于setnx
- XX:當(dāng)key存在的時候,覆蓋key
set lock pay ex 10 NX
get lock
set lock order ex 10 NX
set lock pay ex 10 NX
get lock
1.1.2 hash
Map<String,Map<Object,object>>
一次設(shè)置一個字段值
HSET key field value
一次獲取一個字段值
HGET key field
一次設(shè)置多個字段值
HMSET key field value [field value ...]
一次獲取多個字段值
HMGETkey field [field ....]
獲取所有字段值
hgetall key
獲取某個key內(nèi)的全部數(shù)量
hlen
刪除一個key
hdel
應(yīng)用場景
購物車早期,當(dāng)前小中廠可用
新增商品
hset shopcar:uid1024 334488 1
hset shopcar:uid1024 334477 1
增加商品數(shù)量
hincrby shopcar:uid1024 334477 1
商品總數(shù)
hlen shopcar:uid1024
全部選擇
hgetall shopcar:uid124
1.1.3 list
向列表左邊添加元素
LPUSH key value [value ...]
向列表右邊添加元素
RPUSH key value [value ....]
查看列表
LRANGE key start stop
獲取列表中元素的個數(shù)
LLEN key
應(yīng)用場景
微信文章訂閱公眾號
大V作者李永樂老師在CSDN發(fā)布了文章分別是11和22
陽哥關(guān)注了他們兩2上,只要他們發(fā)布了新文章,就會安裝進(jìn)我的List
lpush likearticle:陽哥id 11 22
查看陽哥自己的號的訂閱的全部文章,類似分布,下面0-10就是一次顯示10條
lrange likearticle:陽哥id 0 10
1.1.4 set
添加元素
SADD key member[member ...]
刪除元素
SREM key member [member ...]
獲取集合中的所有元素
SMEMBERS key
判斷元素是否在集合中
SISMEMBER key member
獲取集合中的元素個數(shù)
SCARD key
從集合中隨機(jī)彈出一個元素,元素不刪除
SRANDMEMBER key [數(shù)字]
從集合中隨機(jī)彈出一個元素,出一個刪一個
SPOP key[數(shù)字]
集合運(yùn)算
集合的差集運(yùn)算A-B 屬于A但不屬于B的元素構(gòu)成的集合
SDIFF key [key ...]
集合的交集運(yùn)算A∩B 屬于A同時也屬于B的共同擁有的元素構(gòu)成的集合
SINTER key [key ...]
集合的并集運(yùn)算AUB 屬于A或者屬于B的元素合并后的集合
SUNION key [key ...]
應(yīng)用場景
微信抽獎小程序
| 1 用戶ID,立即參與按鈕 | sadd key 用戶ID |
|---|---|
| 2 顯示已經(jīng)有多少人參與了,上圖23208人參加 | SCARD key |
| 3 抽獎(從set中任意選取N個中獎人) | SRANDMEMBER key 2 隨機(jī)抽獎2個人,元素不刪除 SPOP key3 隨機(jī)抽獎3個人,元素會刪除 |
微信朋友圈點(diǎn)贊
| 1 新增點(diǎn)贊 | sadd pub:msgID 點(diǎn)贊用戶ID1 ID2 |
|---|---|
| 2 取消點(diǎn)贊 | srem pub:msgID 點(diǎn)贊用戶ID |
| 3 展示所有點(diǎn)贊過的用戶 | SMEMBERS pub:msgID |
| 4 點(diǎn)贊用戶統(tǒng)計 | scard pub:msgID |
| 5 判斷某個朋友是否為樓主點(diǎn)贊過 | SISMEMBER pub:msgID 用戶ID |
微博好友關(guān)注社交關(guān)系

共同關(guān)注的人
sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7
取交集
SINTER s1 s2
共同關(guān)注:我去到以局座張召忠的微博,馬上獲得我和局座共同關(guān)注的人
我關(guān)注的人也關(guān)注他(大家愛好相同)
我關(guān)注了華為余承東,余承東也關(guān)注了局座召忠,我和余總有共同的愛好
sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7
SISMEMBER s1 3
SISMEMBER s2 3
QQ內(nèi)推可能認(rèn)識的人
sadd s1 1 2 3 4 5
sadd s2 3 4 5 6 7
SINTER s1 s2
1) "3"
2) "4"
3) "5"
SDIFF s1 s2
1) "1"
2) "2"
SDIFF s2 s1
1) "6"
2) "7"
1.1.5 zset
向有序集合中加入一個元素和該元素的分?jǐn)?shù)
添加元素
ZADD key score member [score member ...]
按照元素分?jǐn)?shù)從小到大的順序 返回索引從start到stop之間的所有元素
ZRANGE key start stop [WITHSCORES]
獲取元素的分?jǐn)?shù)
ZSCORE key member
刪除元素
ZREM key member [member ...]
獲取指定分?jǐn)?shù)范圍的元素
ZRANGEBYsCORE key min max [WITHSCORES] [LIMIT offset count]
增加某個元素的分?jǐn)?shù)
ZINCRBY key increment member
獲取集合中元素的數(shù)量
ZCARD key
獲得指定分?jǐn)?shù)范圍內(nèi)的元素個數(shù)
ZCOUNT key min max
按照排名范圍刪除元素
ZREMRANGEBYRANK key start stop
獲取元素的排名
從小到大 ZRANK key member
從大到小 ZREVRANK key member
應(yīng)用場景
根據(jù)商品銷售對商品進(jìn)行排序顯示
思路:定義商品銷售排行榜(sorted set集合),key為goods:sellsort,分?jǐn)?shù)為商品銷售數(shù)量。
| 商品編號1001的銷量是9,商品編號1002的銷量是15 | zadd goods:sellsort 9 1001 15 1002 |
|---|---|
| 有一個客戶又買了2件商品1001,商品編號1001銷量加2 | zincrby goods:sellsort 2 1001 |
| 求商品銷量前10名 | ZRANGE goods:sellsort 0 10 withscores |
抖音熱搜
| 1點(diǎn)擊視頻 | ZINCRBY hotvcr:20200919 1八佰 ZINCRBY hotvcr:20200919 15 八佰 2 花木蘭 |
|---|---|
| 2 展示當(dāng)日排行前10條 | ZREVRANGE hotvcr:20200919 0 9 withscores |
1.1.6 Bitmap
BitMap 原本的含義是用一個比特位來映射某個元素的狀態(tài)。由于一個比特位只能表示 0 和 1 兩種狀態(tài),所以 BitMap 能映射的狀態(tài)有限,但是使用比特位的優(yōu)勢是能大量的節(jié)省內(nèi)存空間。
在 Redis 中,可以把 Bitmaps 想象成一個以比特位為單位的數(shù)組,數(shù)組的每個單元只能存儲0和1,數(shù)組的下標(biāo)在 Bitmaps 中叫做偏移量。
Redis 其實(shí)只支持 5 種數(shù)據(jù)類型,并沒有 BitMap 這種類型,BitMap 底層是基于 Redis 的字符串類型實(shí)現(xiàn)的。
# 設(shè)置值,其中value只能是 0 和 1
setbit key offset value
# 獲取值
getbit key offset
# 獲取指定范圍內(nèi)值為 1 的個數(shù)
# start 和 end 以字節(jié)為單位
bitcount key start end
# BitMap間的運(yùn)算
# operations 位移操作符,枚舉值
AND 與運(yùn)算 &
OR 或運(yùn)算 |
XOR 異或 ^
NOT 取反 ~
# result 計算的結(jié)果,會存儲在該key中
# key1 … keyn 參與運(yùn)算的key,可以有多個,空格分割,not運(yùn)算只能一個key
# 當(dāng) BITOP 處理不同長度的字符串時,較短的那個字符串所缺少的部分會被看作 0。返回值是保存到 destkey 的字符串的長度(以字節(jié)byte為單位),和輸入 key 中最長的字符串長度相等。
bitop [operations] [result] [key1] [keyn…]
# 返回指定key中第一次出現(xiàn)指定value(0/1)的位置
bitpos [key] [value]
應(yīng)用場景
1. 用戶簽到
很多網(wǎng)站都提供了簽到功能,并且需要展示最近一個月的簽到情況,這種情況可以使用 BitMap 來實(shí)現(xiàn)。
根據(jù)日期 offset = (今天是一年中的第幾天) % (今年的天數(shù)),key = 年份:用戶id。
如果需要將用戶的詳細(xì)簽到信息入庫的話,可以考慮使用一個異步線程來完成。
2. 統(tǒng)計活躍用戶(用戶登陸情況)
使用日期作為 key,然后用戶 id 為 offset,如果當(dāng)日活躍過就設(shè)置為1。具體怎么樣才算活躍這個標(biāo)準(zhǔn)大家可以自己指定。
假如 20201009 活躍用戶情況是: [1,0,1,1,0]
20201010 活躍用戶情況是 :[ 1,1,0,1,0 ]
統(tǒng)計連續(xù)兩天活躍的用戶總數(shù):
bitop and dest1 20201009 20201010
# dest1 中值為1的offset,就是連續(xù)兩天活躍用戶的ID
bitcount dest1
統(tǒng)計20201009 ~ 20201010 活躍過的用戶:
bitop or dest2 20201009 20201010
3. 統(tǒng)計用戶是否在線
如果需要提供一個查詢當(dāng)前用戶是否在線的接口,也可以考慮使用 BitMap 。即節(jié)約空間效率又高,只需要一個 key,然后用戶 id 為 offset,如果在線就設(shè)置為 1,不在線就設(shè)置為 0。
4. 實(shí)現(xiàn)布隆過濾器
大數(shù)據(jù)簽到、重復(fù)點(diǎn)贊、訂單重復(fù)評論等
# 假設(shè)某用戶id=10,有篇文章叫artcleA,則設(shè)置artcleA上的位10成1
127.0.0.1:6379> setbit artcleA 10 1
(integer) 0
# 統(tǒng)計文章artcleA的點(diǎn)贊人數(shù)
127.0.0.1:6379> bitcount artcle
(integer) 1
# 模擬重復(fù)點(diǎn)贊同一篇文章,返回了1,就可以根據(jù)返回值避免重復(fù)點(diǎn)贊
127.0.0.1:6379> setbit artcleA 10 1
(integer) 1
1.1.7 HyperLogLog
- Redis HyperLogLog 是用來做基數(shù)統(tǒng)計的算法,HyperLogLog 的優(yōu)點(diǎn)是,在輸入元素的數(shù)量或者體積非常非常大時,計算基數(shù)所需的空間總是固定 的、并且是很小的。
- 在 Redis 里面,每個 HyperLogLog 鍵只需要花費(fèi) 12 KB 內(nèi)存,就可以計算接近 2^64 個不同元素的基 數(shù)。這和計算基數(shù)時,元素越多耗費(fèi)內(nèi)存就越多的集合形成鮮明對比。
- 但是,因?yàn)?HyperLogLog 只會根據(jù)輸入元素來計算基數(shù),而不會儲存輸入元素本身,所以 HyperLogLog 不能像集合那樣,返回輸入的各個元素
pfadd 添加
影響基數(shù)估值則返回1否則返回0.若key不存在則創(chuàng)建
時間復(fù)雜度O(1)
pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
pfcount 獲得基數(shù)值
得到基數(shù)值,白話就叫做去重值(1,1,2,2,3)的插入pfcount得到的是3
可一次統(tǒng)計多個key
時間復(fù)雜度為O(N),N為key的個數(shù)
返回值是一個帶有 0.81% 標(biāo)準(zhǔn)錯誤(standard error)的近似值.
pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
pfcount m1
(integer) 4
pfmerge 合并多個key
取多個key的并集
命令只會返回 OK.
時間復(fù)雜度為O(N)
pfadd m1 1 2 3 4 1 2 3 2 2 2 2
(integer) 1
pfcount m1
(integer) 4
pfadd m2 3 3 3 4 4 4 5 5 5 6 6 6 1
(integer) 1
pfcount m2
(integer) 5
pfmerge mergeDes m1 m2
OK
pfcount mergeDes
(integer) 6
應(yīng)用場景
說明:
- 基數(shù)不大,數(shù)據(jù)量不大就用不上,會有點(diǎn)大材小用浪費(fèi)空間
- 有局限性,就是只能統(tǒng)計基數(shù)數(shù)量,而沒辦法去知道具體的內(nèi)容是什么
- 和bitmap相比,屬于兩種特定統(tǒng)計情況,簡單來說,HyperLogLog 去重比 bitmap 方便很多
- 一般可以bitmap和hyperloglog配合使用,bitmap標(biāo)識哪些用戶活躍,hyperloglog計數(shù)
一般使用:
- 統(tǒng)計注冊 IP 數(shù)
- 統(tǒng)計每日訪問 IP 數(shù)
- 統(tǒng)計頁面實(shí)時 UV 數(shù)
- 統(tǒng)計在線用戶數(shù)
- 統(tǒng)計用戶每天搜索不同詞條的個數(shù)
1.1.8 GEO
Redis3.2 版本提供了GEO(地理信息定位)功能,支持存儲地理位置信息用來實(shí)現(xiàn)諸如附近位置、搖一搖這類依賴于地理位置信息的功能,對于需要實(shí)現(xiàn)這些功能的開發(fā)者來說是一大福音。GEO功能是Redis的另一位作者M(jìn)att Stancliff 借鑒NoSQL數(shù)據(jù)庫 Ardb 實(shí)現(xiàn)的,Ardb的作者來自中國,它提供了優(yōu)秀的GEO功能。
Redis GEO 相關(guān)的命令如下:
# 添加一個空間元素,longitude、latitude、member分別是該地理位置的經(jīng)度、緯度、成員
# 這里的成員就是指代具體的業(yè)務(wù)數(shù)據(jù),比如說用戶的ID等
# 需要注意的是Redis的緯度有效范圍不是[-90,90]而是[-85,85]
# 如果在添加一個空間元素時,這個元素中的menber已經(jīng)存在key中,那么GEOADD命令會返回0,相當(dāng)于更新了這個menber的位置信息
GEOADD key longitude latitude member [longitude latitude member]
# 用于添加城市的坐標(biāo)信息
geoadd cities:locations 117.12 39.08 tianjin 114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding
# 獲取地理位置信息
geopos key member [member ...]
# 獲取天津的坐標(biāo)
geopos cities:locations tianjin
# 獲取兩個坐標(biāo)之間的距離
# unit代表單位,有4個單位值
- m (meter) 代表米
- km (kilometer)代表千米
- mi (miles)代表英里
- ft (ft)代表尺
geodist key member1 member2 [unit]
# 獲取天津和保定之間的距離
GEODIST cities:locations tianjin baoding km
# 獲取指定位置范圍內(nèi)的地理信息位置集合,此命令可以用于實(shí)現(xiàn)附近的人的功能
# georadius和georadiusbymember兩個命令的作用是一樣的,都是以一個地理位置為中心算出指定半徑內(nèi)的其他地理信息位置,不同的是georadius命令的中心位置給出了具體的經(jīng)緯度,georadiusbymember只需給出成員即可。其中radiusm|km|ft|mi是必需參數(shù),指定了半徑(帶單位),這兩個命令有很多可選參數(shù),參數(shù)含義如下:
# - withcoord:返回結(jié)果中包含經(jīng)緯度。
# - withdist:返回結(jié)果中包含離中心節(jié)點(diǎn)位置的距離。
# - withhash:返回結(jié)果中包含geohash,有關(guān)geohash后面介紹。
# - COUNT count:指定返回結(jié)果的數(shù)量。
# - asc|desc:返回結(jié)果按照離中心節(jié)點(diǎn)的距離做升序或者降序。
# - store key:將返回結(jié)果的地理位置信息保存到指定鍵。
# - storedist key:將返回結(jié)果離中心節(jié)點(diǎn)的距離保存到指定鍵。
georadius key longitude latitude radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
georadiusbymember key member radiusm|km|ft|mi [withcoord] [withdist] [withhash] [COUNT count] [asc|desc] [store key] [storedist key]
# 獲取geo hash
# Redis使用geohash將二維經(jīng)緯度轉(zhuǎn)換為一維字符串,geohash有如下特點(diǎn):
# - GEO的數(shù)據(jù)類型為zset,Redis將所有地理位置信息的geohash存放在zset中。
# - 字符串越長,表示的位置更精確,表3-8給出了字符串長度對應(yīng)的精度,例如geohash長度為9時,精度在2米左右。長度和精度的對應(yīng)關(guān)系,請參考:https://easyreadfs.nosdn.127.net/9F42_CKRFsfc8SUALbHKog==/8796093023252281390
# - 兩個字符串越相似,它們之間的距離越近,Redis利用字符串前綴匹配算法實(shí)現(xiàn)相關(guān)的命令。
# - geohash編碼和經(jīng)緯度是可以相互轉(zhuǎn)換的。
# - Redis正是使用有序集合并結(jié)合geohash的特性實(shí)現(xiàn)了GEO的若干命令。
geohash key member [member ...]
# 刪除操作,GEO沒有提供刪除成員的命令,但是因?yàn)镚EO的底層實(shí)現(xiàn)是zset,所以可以借用zrem命令實(shí)現(xiàn)對地理位置信息的刪除。
zrem key member
1.2 知道分布式鎖嗎?有哪些實(shí)現(xiàn)方案? 你談?wù)剬edis分布式鎖的理解, 刪key的時候有什么問題?
- 分布式鎖的面試題
- Redis除了拿來做緩存,你還見過基于Redis的什么用法?
- Redis做分布式鎖的時候有需要注意的問題?
- 如果是Redis是單點(diǎn)部署的,會帶來什么問題?
- 那你準(zhǔn)備怎么解決單點(diǎn)問題呢?
- 集群模式下,比如主從模式,有沒有什么問題呢?
- 那你簡單的介紹一下Redlock吧?你簡歷上寫redisson,你談?wù)?/li>
- Redis分布式鎖如何續(xù)期?看門狗知道嗎?
(boot+redis)
使用場景: 多個服務(wù)間保證同一時刻同一時間段內(nèi)同一用戶只能有一個請求(防止關(guān)鍵業(yè)務(wù)出現(xiàn)并發(fā)攻擊)
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}
上面代碼優(yōu)化
1.2.1 單機(jī)版沒加鎖
沒有加鎖,并發(fā)下數(shù)字不對,出現(xiàn)超賣現(xiàn)象
加synchronized 加ReentrantLock
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
private final Lock lock = new ReentrantLock();
@GetMapping("/buy_goods")
public String buy_Goods(){
if (lock.tryLock()){
try {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
lock.unlock();
}
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}
@RestController
public class GoodController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
synchronized (this) {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}
}
說明:在單機(jī)環(huán)境下,可以使用synchronized或Lock來實(shí)現(xiàn)。
但是在分布式系統(tǒng)中,因?yàn)楦偁幍木€程可能不在同一個節(jié)點(diǎn)上(同一個jvm中),所以需要一個讓所有進(jìn)程都能訪問到的鎖來實(shí)現(xiàn),比如redis或者zookeeper來構(gòu)建;
不同進(jìn)程jvm層面的鎖就不管用了,那么可以利用第三方的一個組件,來獲取鎖,未獲取到鎖,則阻塞當(dāng)前想要運(yùn)行的線程
1.2.2 nginx分布式微服務(wù)架構(gòu)
分布式部署后,單機(jī)鎖還是出現(xiàn)超賣現(xiàn)象,需要分布式鎖
Nginx配置負(fù)載均衡
啟動Nginx并測試通過
/usr/local/nginx/conf
重啟 ./nginx -s reload
/usr/local/nginx/sbin/nginx-c /usr/local/nginx/conf/nginx.conf
./nginx-c /usr/local/nginx/conf/nginx.conf
高并發(fā)模擬 jmeter壓測

解決:
上redis分布式鎖setnx
Redis具有極高的性能,且其命令對分布式鎖支持友好,借助SET命令即可實(shí)現(xiàn)加鎖處理
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
stringRedisTemplate.delete(REDIS_LOCK_KEY);//釋放鎖
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}
}
出異常的話,可能無法釋放鎖, 必須要在代碼層面finally釋放鎖
加鎖解鎖,lock/unlock必須同時出現(xiàn)并保證調(diào)用
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try{
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//釋放鎖
}
}
部署了微服務(wù)jar包的機(jī)器掛了,代碼層面根本沒有走到finally這塊, 沒辦法保證解鎖,這個key沒有被刪除,需要加入一個過期時間限定key
需要對lockKey有過期時間的設(shè)定
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try{
//setIfAbsent() 就是如果不存在就新建
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value);//setnx
stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//釋放鎖
}
}
}
設(shè)置key+過期時間分開了,必須要合并成一行具備原子性
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try{
//setIfAbsent() == setnx 就是如果不存在就新建,同時加上過期時間保證原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
stringRedisTemplate.delete(REDIS_LOCK_KEY);//釋放鎖
}
}
}
張冠李戴,刪除了別人的鎖

只能自己刪除自己的,不許動別人的
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try{
//setIfAbsent() == setnx 就是如果不存在就新建,同時加上過期時間保證原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
stringRedisTemplate.delete(REDIS_LOCK_KEY);//釋放鎖
}
}
}
}
finally塊的判斷+del刪除操作不是原子性的
解決:用redis自身的事務(wù)
-
事務(wù)介紹
- Redis的事務(wù)是通過MULTI,EXEC,DISCARD和WATCH這四個命令來完成。
- Redis的單個命令都是原子性的,所有這里確保事務(wù)性的對象是命令集合。
- Redis將命令集合序列化并確保處于一事務(wù)的命令集合連接且不被打斷的執(zhí)行。
- Redis不支持回滾的操作。
相關(guān)命令
-
MULTI 注:用于標(biāo)記事務(wù)塊的開始。
- Redis會將后續(xù)的命令逐個放入隊(duì)列中,然后使用EXEC命令原子化的執(zhí)行這個命令序列。
-
EXEC
- 在一個事務(wù)中執(zhí)行所有先前放入到隊(duì)列的命令,然后恢復(fù)正常的連接狀態(tài)。
-
DISCARD
- 清除所有先前在一個事務(wù)中放入隊(duì)列的命令,然后恢復(fù)正常的連接狀態(tài)。
-
WATCH
- 當(dāng)某個事務(wù)需要按條件執(zhí)行時,就要使用這個命令將給定的鍵狀態(tài)設(shè)置為受監(jiān)控的狀態(tài)。
-
UNWATCH
- 清除所有先前為一個事務(wù)設(shè)置監(jiān)控的鍵。
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try{
//setIfAbsent() == setnx 就是如果不存在就新建,同時加上過期時間保證原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
while (true)
{
stringRedisTemplate.watch(REDIS_LOCK_KEY); //加事務(wù),樂觀鎖
if (value.equalsIgnoreCase(stringRedisTemplate.opsForValue().get(REDIS_LOCK_KEY))){
stringRedisTemplate.setEnableTransactionSupport(true);
stringRedisTemplate.multi();//開始事務(wù)
stringRedisTemplate.delete(REDIS_LOCK_KEY);
List<Object> list = stringRedisTemplate.exec();
if (list == null) { //如果等于null,就是沒有刪掉,刪除失敗,再回去while循環(huán)那再重新執(zhí)行刪除
continue;
}
}
//如果刪除成功,釋放監(jiān)控器,并且breank跳出當(dāng)前循環(huán)
stringRedisTemplate.unwatch();
break;
}
}
}
}
用Lua腳本 Redis可以通過eval命令保證代碼執(zhí)行的原子性
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@GetMapping("/buy_goods")
public String buy_Goods() throws Exception{
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
try{
//setIfAbsent() == setnx 就是如果不存在就新建,同時加上過期時間保證原子性
Boolean lockFlag = stringRedisTemplate.opsForValue().setIfAbsent(REDIS_LOCK_KEY, value,10L, TimeUnit.SECONDS);
stringRedisTemplate.expire(REDIS_LOCK_KEY,10L, TimeUnit.SECONDS);
if (!lockFlag) {
return "搶鎖失敗,┭┮﹏┭┮";
}else {
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}
}finally {
Jedis jedis = RedisUtils.getJedis();
String script = "if redis.call('get', KEYS[1]) == ARGV[1]"+"then "
+"return redis.call('del', KEYS[1])"+"else "+ " return 0 " + "end";
try{
Object result = jedis.eval(script, Collections.singletonList(REDIS_LOCK_KEY), Collections.singletonList(value));
if ("1".equals(result.toString())){
System.out.println("------del REDIS_LOCK_KEY success");
}else {
System.out.println("------del REDIS_LOCK_KEY error");
}
}finally {
if (null != jedis){
jedis.close();
}
}
}
}
}
1.2.3 確保redisLock過期時間大于業(yè)務(wù)執(zhí)行時間的問題
Redis(AP)分布式鎖如何續(xù)期?
集群+CAP對比zookeeper(CP)
redis異步復(fù)制造成的鎖丟失, 比如:主節(jié)點(diǎn)沒來的及把剛剛set進(jìn)來這條數(shù)據(jù)給從節(jié)點(diǎn),就掛了。
此時如果集群模式下,就得上Redisson來解決
redis集群環(huán)境下,我們自己寫的也不OK, 直接上RedLock之Redisson落地實(shí)現(xiàn)
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
try{
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}finally {
redissonLock.unlock();
}
}
}
IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id:
出現(xiàn)這個錯誤的原因:是在并發(fā)多的時候就可能會遇到這種錯誤,可能會被重新?lián)屨?/em>
不見得當(dāng)前這個鎖的狀態(tài)還是在鎖定,并且本線程持有
@RestController
public class GoodController {
public static final String REDIS_LOCK_KEY = "lockhhf";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Value("${server.port}")
private String serverPort;
@Autowired
private Redisson redisson;
@GetMapping("/buy_goods")
public String buy_Goods(){
String value = UUID.randomUUID().toString()+Thread.currentThread().getName();
RLock redissonLock = redisson.getLock(REDIS_LOCK_KEY);
redissonLock.lock();
try{
String result = stringRedisTemplate.opsForValue().get("goods:001");
int goodsNumber = result == null ? 0 : Integer.parseInt(result);
if (goodsNumber > 0){
int realNumber = goodsNumber - 1;
stringRedisTemplate.opsForValue().set("goods:001",realNumber + "");
System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort);
return "你已經(jīng)成功秒殺商品,此時還剩余:" + realNumber + "件"+"\t 服務(wù)器端口: "+serverPort;
}else {
System.out.println("商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort);
}
return "商品已經(jīng)售罄/活動結(jié)束/調(diào)用超時,歡迎下次光臨"+"\t 服務(wù)器端口: "+serverPort;
}finally {
//還在持有鎖的狀態(tài),并且是當(dāng)前線程持有的鎖再解鎖
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()){
redissonLock.unlock();
}
}
}
}
1.2.4 總結(jié)
synchronized 單機(jī)版oK,上分布式
nginx分布式微服務(wù) 單機(jī)鎖不行
取消單機(jī)鎖 上redis分布式鎖setnx
只加了鎖,沒有釋放鎖, 出異常的話,可能無法釋放鎖, 必須要在代碼層面finally釋放鎖
宕機(jī)了,部署了微服務(wù)代碼層面根本沒有走到finally這塊,
沒辦法保證解鎖,這個key沒有被刪除,
需要有l(wèi)ockKey的過期時間設(shè)定
為redis的分布式鎖key,增加過期時間
此外,還必須要setnx+過期時間必須同一行的原子性操作
必須規(guī)定只能自己刪除自己的鎖,
你不能把別人的鎖刪除了,
防止張冠李戴,1刪2,2刪3
lua或者事務(wù)
redis集群環(huán)境下,我們自己寫的也不OK
直接上RedLock之Redisson落地實(shí)現(xiàn)
1.3 redis緩存過期淘汰策略
- 生產(chǎn)上你們的redis內(nèi)存設(shè)置多少?
- 如何配置、修改redis的內(nèi)存大小
- 如果內(nèi)存滿了你怎么辦
- redis清理內(nèi)存的方式?定期刪除和惰性刪除了解過嗎
- redis緩存淘汰策略
- redis的LRu了解過嗎?可否手寫一個LRu算法
1.3.1 Redis內(nèi)存滿了怎么辦
redis默認(rèn)內(nèi)存多少?在哪里查看? 如何設(shè)置修改?
查看Redis最大占用內(nèi)存
打開redis配置文件,設(shè)置maxmemory參數(shù),maxmemory是bytes字節(jié)類型,注意轉(zhuǎn)換。
redis默認(rèn)內(nèi)存多少可以用?
如果不設(shè)置內(nèi)存大小或者內(nèi)存大小設(shè)置為0,在64位操作系統(tǒng)下不限制內(nèi)存大小,在32位操作系統(tǒng)下最多使用3gb
一般生產(chǎn)上你如何配置?
一般推薦Redis設(shè)置內(nèi)存為最大物理內(nèi)存的四分之三,也就是0.75
如何修改redis內(nèi)存設(shè)置
通過修改文件配置 maxmemory
通過命令修改 config set maxmemory xx
什么命令查看redis內(nèi)存使用情況?
info memory
真要打滿了會怎么樣? 如果Redis內(nèi)存使用超出了設(shè)置的最大值會怎樣?
改改配置,故意把最大值設(shè)為1個byte試試
config set maxmemory 1
config get maxmemory
set k1 v1
(error) OOM command not allowed when used memory > 'maxmemory'.
結(jié)論
- 設(shè)置了maxmemory的選項(xiàng),假如redis內(nèi)存使用達(dá)到上限
- 沒有加上過期時間就會導(dǎo)致數(shù)據(jù)寫滿maxmemory 為了避免類似情況,引出內(nèi)存淘汰策略
1.3.2 redis緩存淘汰策略
往redis里寫的數(shù)據(jù)是怎么沒了的
redis過期鍵的刪除策略
如果一個鍵是過期的,那它到了過期時間之后是不是馬上就從內(nèi)存中被被刪除呢??
如果回答yes,你自己走還是面試官送你?
如果不是,那過期后到底什么時候被刪除呢??是個什么操作?
三種不同的刪除策略
定期刪除
Redis不可能時時刻刻遍歷所有被設(shè)置了生存時間的key,來檢測數(shù)據(jù)是否已經(jīng)到達(dá)過期時間,然后對它進(jìn)行刪除。
立即刪除能保證內(nèi)存中數(shù)據(jù)的最大新鮮度,因?yàn)樗WC過期鍵值會在過期后馬上被刪除,其所占用的內(nèi)存也會隨之釋放。但是立即刪除對cpu是最不友好的。因?yàn)閯h除操作會占用cpu的時間,如果剛好碰上了cpu很忙的時候,比如正在做交集或排序等計算的時候,就會給cpu造成額外的壓力,讓CPU心累,時時需要刪除,忙死。。。。。。
這會產(chǎn)生大量的性能消耗,同時也會影響數(shù)據(jù)的讀取操作。
總結(jié):對CPU不友好,用處理器性能換取存儲空間(拿時間換空間)
惰性刪除
數(shù)據(jù)到達(dá)過期時間,不做處理。等下次訪問該數(shù)據(jù)時,
如果未過期,返回數(shù)據(jù);
發(fā)現(xiàn)已過期,刪除,返回不存在。
惰性刪除策略的缺點(diǎn)是,它對內(nèi)存是最不友好的。
如果一個鍵已經(jīng)過期,而這個鍵又仍然保留在數(shù)據(jù)庫中,那么只要這個過期鍵不被刪除,它所占用的內(nèi)存就不會釋放。
在使用惰性刪除策略時,如果數(shù)據(jù)庫中有非常多的過期鍵,而這些過期鍵又恰好沒有被訪問到的話,那么它們也許永遠(yuǎn)也不會被刪除(除非用戶手動執(zhí)行FLUSHDB),我們甚至可以將這種情況看作是一種內(nèi)存泄漏–無用的垃圾數(shù)據(jù)占用了大量的內(nèi)存,而服務(wù)器卻不會自己去釋放它們,這對于運(yùn)行狀態(tài)非常依賴于內(nèi)存的Redis服務(wù)器來說,肯定不是一個好消息
總結(jié):對memory不友好,用存儲空間換取處理器性能(拿空間換時間)
上面兩種方案都走極端
定期刪除策略是前兩種策略的折中:
定期刪除策略每隔一段時間執(zhí)行一次刪除過期鍵操作,并通過限制刪除操作執(zhí)行的時長和頻率來減少刪除操作對CPU時間的影響。
周期性輪詢redis庫中的時效性數(shù)據(jù),采用隨機(jī)抽取的策略,利用過期數(shù)據(jù)占比的方式控制刪除頻度
特點(diǎn)1:CPU性能占用設(shè)置有峰值,檢測頻度可自定義設(shè)置
特點(diǎn)2:內(nèi)存壓力不是很大,長期占用內(nèi)存的冷數(shù)據(jù)會被持續(xù)清理
總結(jié):周期性抽查存儲空間(隨機(jī)抽查,重點(diǎn)抽查)
舉例:redis默認(rèn)每個100ms檢查,是否有過期的key,有過期key則刪除。注意:redis不是每隔100ms將所有的key檢查一次而是隨機(jī)抽取進(jìn)行檢查(如果每隔100ms,全部key進(jìn)行檢查,redis直接進(jìn)去ICU)。因此,如果只采用定期刪除策略,會導(dǎo)致很多key到時間沒有刪除。
定期刪除策略的難點(diǎn)是確定刪除操作執(zhí)行的時長和頻率:如果刪除操作執(zhí)行得太頻繁,或者執(zhí)行的時間太長,定期刪除策略就會退化成定時刪除策略,以至于將CPU時間過多地消耗在刪除過期鍵上面。如果刪除操作執(zhí)行得太少,或者執(zhí)行的時間太短,定期刪除策略又會和惰性刪除束略一樣,出現(xiàn)浪費(fèi)內(nèi)存的情況。因此,如果采用定期刪除策略的話,服務(wù)器必須根據(jù)情況,合理地設(shè)置刪除操作的執(zhí)行時長和執(zhí)行頻率。
定期抽樣key,判斷是否過期
漏網(wǎng)之魚
1 定期刪除時,從來沒有被抽查到
2 惰性刪除時,也從來沒有被點(diǎn)中使用過
上述2步驟======> 大量過期的key堆積在內(nèi)存中,導(dǎo)致redis內(nèi)存空間緊張或者很快耗盡
必須要有一個更好的兜底方案......
內(nèi)存淘汰策略
noeviction: 不會驅(qū)逐任何key
allkeys-lru: 對所有key使用LRU算法進(jìn)行刪除
volatile-lru: 對所有設(shè)置了過期時間的key使用LRU算法進(jìn)行刪除
allkeys-random: 對所有key隨機(jī)刪除
volatile-random: 對所有設(shè)置了過期時間的key隨機(jī)刪除
volatile-ttl: 刪除馬上要過期的key
allkeys-lfu: 對所有key使用LFU算法進(jìn)行刪除
volatile-lfu: 對所有設(shè)置了過期時間的key使用LFU算法進(jìn)行刪除
總結(jié)
2*4得8
2個維度
過期鍵中篩選
所有鍵中篩選
4個方面
LRU LFU random ttl
如何配置、修改
config set maxmemory-policy allkeys-lru
或者配置文件修改
1.4 redis的LRU算法簡介
redis的LRU了解過嗎? 可否手寫一個LRU算法
LRU是Least Recently Used的縮寫,即最近最少使用,是一種常用的頁面置換算法,
選擇最近最久未使用的數(shù)據(jù)予以淘汰。
算法來源 https://leetcode-cn.com/problems/lru-cache/

設(shè)計思想
1 所謂緩存,必須要有讀+寫兩個操作,按照命中率的思路考慮,寫操作+讀操作時間復(fù)雜度都需要為O(1)
2 特性要求分析
2.1 必須有順序之分,以區(qū)分最近使用的和很久沒用到的數(shù)據(jù)排序。
2.2 寫和讀操作 一次搞定。
2.3 如果容量(坑位)滿了要刪除最不長用的數(shù)據(jù),每次新訪問還要把新的數(shù)據(jù)插入到隊(duì)頭(按照業(yè)務(wù)你自己設(shè)定左右那一邊是隊(duì)頭)
查找快,插入快,刪除快,且還需要先后排序-------->什么樣的數(shù)據(jù)結(jié)構(gòu)滿足這個問題?
你是否可以在O(1)時間復(fù)雜度內(nèi)完成這兩種操作?
如果一次就可以找到,你覺得什么數(shù)據(jù)結(jié)構(gòu)最合適??
LRU的算法核心是哈希鏈表
本質(zhì)就是HashMap+DoubleLinkedList 時間復(fù)雜度是O(1),哈希表+雙向鏈表的結(jié)合體
編碼手寫如何實(shí)現(xiàn)LRU
參考LinkedHashMap,依賴JDK
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCacheDemo<K,V> extends LinkedHashMap<K, V> {
//緩存坑位
private int capacity;
public LRUCacheDemo(int capacity) {
super(capacity,0.75F,false);
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return super.size() > capacity;
}
public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
lruCacheDemo.put(1,"a");
lruCacheDemo.put(2,"b");
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(4,"d");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(3,"c");
System.out.println(lruCacheDemo.keySet());
lruCacheDemo.put(5,"x");
System.out.println(lruCacheDemo.keySet());
}
}
/**
* true
* [1, 2, 3]
* [2, 3, 4]
* [2, 4, 3]
* [2, 4, 3]
* [2, 4, 3]
* [4, 3, 5]
* */
/**
[1, 2, 3]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[3, 4, 5]
*/
不依賴JDK
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUCacheDemo{
//map負(fù)責(zé)查找,構(gòu)建一個虛擬的雙向鏈表,它里面安裝的就是一個個Node節(jié)點(diǎn),作為數(shù)據(jù)載體。
//1.構(gòu)造一個node節(jié)點(diǎn)作為數(shù)據(jù)載體
class Node<K, V> {
K key;
V value;
Node<K,V> prev;
Node<K,V> next;
public Node(){
this.prev = this.next = null;
}
public Node(K key, V value) {
this.key = key;
this.value = value;
this.prev = this.next = null;
}
}
//2 構(gòu)建一個虛擬的雙向鏈表,,里面安放的就是我們的Node
class DoubleLinkedList<K, V> {
Node<K, V> head;
Node<K, V> tail;
public DoubleLinkedList() {
head = new Node<>();
tail = new Node<>();
head.next = tail;
tail.prev = head;
}
//3. 添加到頭
public void addHead(Node<K,V> node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
//4.刪除節(jié)點(diǎn)
public void removeNode(Node<K, V> node) {
node.next.prev = node.prev;
node.prev.next = node.next;
node.prev = null;
node.next = null;
}
//5.獲得最后一個節(jié)點(diǎn)
public Node getLast() {
return tail.prev;
}
}
private int cacheSize;
Map<Integer,Node<Integer,Integer>> map;
DoubleLinkedList<Integer,Integer> doubleLinkedList;
public LRUCacheDemo(int cacheSize) {
this.cacheSize = cacheSize;//坑位
map = new HashMap<>();//查找
doubleLinkedList = new DoubleLinkedList<>();
}
public int get(int key){
if (!map.containsKey(key)){
return -1;
}
Node<Integer, Integer> node = map.get(key);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
return node.value;
}
public void put(int key, int value) {
if (map.containsKey(key)){ //update
Node<Integer, Integer> node = map.get(key);
node.value = value;
map.put(key, node);
doubleLinkedList.removeNode(node);
doubleLinkedList.addHead(node);
}else {
//坑位滿了
if (map.size() == cacheSize) {
Node<Integer,Integer> lastNode = doubleLinkedList.getLast();
map.remove(lastNode.key);
doubleLinkedList.removeNode(lastNode);
}
//新增一個
Node<Integer, Integer> newNode = new Node<>(key, value);
map.put(key,newNode);
doubleLinkedList.addHead(newNode);
}
}
public static void main(String[] args) {
LRUCacheDemo lruCacheDemo = new LRUCacheDemo(3);
lruCacheDemo.put(1,1);
lruCacheDemo.put(2,2);
lruCacheDemo.put(3,3);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(4,1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(3,1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(3,1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(3,1);
System.out.println(lruCacheDemo.map.keySet());
lruCacheDemo.put(5,1);
System.out.println(lruCacheDemo.map.keySet());
}
}
/**
* true
* [1, 2, 3]
* [2, 3, 4]
* [2, 4, 3]
* [2, 4, 3]
* [2, 4, 3]
* [4, 3, 5]
* */
/**
[1, 2, 3]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[2, 3, 4]
[3, 4, 5]
*/