redis分布式鎖redisson
分布式框架中,普通鎖是滿足不了業(yè)務需求的是晨,分布式鎖在分布式框架中不可缺失肚菠;比如互聯(lián)網(wǎng)秒殺、搶優(yōu)惠券罩缴、接口冪等性校驗蚊逢。redis中存在redisson工具包專門處理redis在分布式鎖的應用。
java中redisson的實現(xiàn)
<!--添加依賴-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency>
@Bean
public Redisson redisson() {
// 此為單機模式
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.0.60:6379").setDatabase(0);
// 此為集群模式
/*config.useClusterServers()
.addNodeAddress("redis://192.168.0.61:8001")
.addNodeAddress("redis://192.168.0.62:8002")
.addNodeAddress("redis://192.168.0.63:8003")
.addNodeAddress("redis://192.168.0.61:8004")
.addNodeAddress("redis://192.168.0.62:8005")
.addNodeAddress("redis://192.168.0.63:8006");*/
//還有哨兵模式
return (Redisson) Redisson.create(config);
}
@Autowired
private Redisson redisson;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock() throws InterruptedException {
String lockKey = "product_001";
RLock redissonLock = redisson.getLock(lockKey);
try {
// 加鎖箫章,實現(xiàn)鎖續(xù)命功能
redissonLock.lock();
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock")); // jedis.get("stock")
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + ""); // jedis.set(key,value)
System.out.println("扣減成功烙荷,剩余庫存:" + realStock + "");
} else {
System.out.println("扣減失敗,庫存不足");
}
}finally {
redissonLock.unlock();
}
return "end";
}
- 使用了jedis.setnx方法加鎖檬寂,只允許設置一次终抽;刪除對應的key進行解鎖。
- 每個線程設置的key對應的value值具有唯一性桶至,最后解鎖刪除value值判斷是否為當前加鎖的線程拿诸,防止了因為當前線程處理時間過長redis本身刪除解鎖后其它線程進來而導致當前線程解了其他線程的鎖,保證了自己加鎖自己解鎖塞茅。
- 增加了鎖續(xù)命的功能。
Redis Lua腳本
Redis在2.6推出了腳本功能季率,允許開發(fā)者使用Lua語言編寫腳本傳到Redis中執(zhí)行野瘦。使用腳本的好處如下:
- 減少網(wǎng)絡開銷:本來5次網(wǎng)絡請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis服務器上完成鞭光。使用腳本吏廉,減少了網(wǎng)絡往返時延。這點跟管道類似惰许。
- 原子操作:Redis會將整個腳本作為一個整體執(zhí)行席覆,中間不會被其他命令插入。管道不是原子的汹买,不過 redis的批量操作命令(類似mset)是原子的佩伤。
- 替代redis的事務功能:redis自帶的事務功能很雞肋,報錯不支持回滾晦毙,而redis的lua腳本幾乎實現(xiàn)了常規(guī)的事務功能生巡,支持報錯回滾操作,官方推薦如果要使用redis的事務功能可以用redis lua替代见妒。
從Redis2.6.0版本開始孤荣,通過內(nèi)置的Lua解釋器,可以使用EVAL命令對Lua腳本進行求值须揣。EVAL命令的格式如下:
EVAL script numkeys key [key ...] arg [arg ...]
script:lua腳本盐股。
numkeys:key的數(shù)量。
key:key值耻卡,這里為list疯汁。
arg:value值,這里為list劲赠,與key相對應涛目。
redis中使用lua腳本
//******* lua腳本示例 ********
//模擬一個商品減庫存的原子操作
//lua腳本命令執(zhí)行方式:redis-cli --eval /tmp/test.lua , 10
jedis.set("product_stock_10016", "15"); //初始化商品10016的庫存
String script = " local count = redis.call('get', KEYS[1]) " +
" local a = tonumber(count) " +
" local b = tonumber(ARGV[1]) " +
" if a >= b then " +
" redis.call('set', KEYS[1], count-b) " +
//模擬語法報錯回滾操作" bb == 0 " +
" return 1 " +
" end " +
" return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);
注意1:多線程在執(zhí)行l(wèi)ua腳本的時候,是不存在并發(fā)問題的凛澎,原因是所有的命令在redis中都會以單線程的形式執(zhí)行霹肝。
注意2:key與value傳參都是以list的類型傳入,同時下標從1開始而不是0塑煎。
lua腳本缺點
redis在高并發(fā)執(zhí)行指令都是串行化沫换,單線程的,如果lua腳本業(yè)務邏輯比較復雜執(zhí)行時間比較長最铁,這會影響redis的性能讯赏;如果lua腳本中出現(xiàn)死循環(huán)的現(xiàn)象,執(zhí)行該腳本的redis集群基本上癱瘓了冷尉。
注意1:redisson加的鎖為可重入鎖漱挎,前提是同一個線程。
注意2:redisson在加鎖后會存在主節(jié)點重新選舉的情況雀哨,這期間可能會導致鎖數(shù)據(jù)丟失磕谅,從而出現(xiàn)鎖失效的現(xiàn)象私爷。
redis RedLock
為解決主節(jié)點重新選舉的情況,這期間可能會導致鎖數(shù)據(jù)丟失問題膊夹,引入RedLock可解決問題衬浑。但是RedLock性能比較差,而且有存在Bug放刨。不推薦使用工秩,建議容忍該問題,若不能容忍进统,推薦使用zookeeper助币。
@RequestMapping("/redlock")
public String redlock() throws InterruptedException {
String lockKey = "product_001";
//這里需要自己實例化不同redis實例的redisson客戶端連接,這里只是偽代碼用一個redisson客戶端簡化了
RLock lock1 = redisson.getLock(lockKey);
RLock lock2 = redisson.getLock(lockKey);
RLock lock3 = redisson.getLock(lockKey);
/**
* 根據(jù)多個 RLock 對象構(gòu)建 RedissonRedLock (最核心的差別就在這里)
*/
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
try {
/**
* 4.嘗試獲取鎖
* waitTimeout 嘗試獲取鎖的最大等待時間麻昼,超過這個值奠支,則認為獲取鎖失敗
* leaseTime 鎖的持有時間,超過這個時間鎖會自動失效(值應設置為大于業(yè)務處理的時間,確保在鎖有效期內(nèi)業(yè)務能處理完)
*/
boolean res = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (res) {
//成功獲得鎖抚芦,在這里處理業(yè)務
}
} catch (Exception e) {
throw new RuntimeException("lock fail");
} finally {
//無論如何, 最后都要解鎖
redLock.unlock();
}
return "end";
}
高并發(fā)場景分布式鎖性能提升
把資源庫存分成多份倍谜,別分存儲在不同的集群內(nèi),把鎖分段分發(fā)叉抡。比如一個商品庫存數(shù)量1000個尔崔,把庫存分為3份,應用key的hash算法把3份庫存分發(fā)的不同的redis子集群內(nèi)褥民。