redis事务原理,使用,详解
生活随笔
收集整理的這篇文章主要介紹了
redis事务原理,使用,详解
小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
聲明:本博客內(nèi)容來自《Redis深度歷險(xiǎn)》一書
? ? ??
為了確保連續(xù)多個(gè)操作的原子性,一個(gè)成熟的數(shù)據(jù)庫通常都會(huì)有事務(wù)支持,Redis 也不例外。Redis 的事務(wù)使用非常簡單,不同于關(guān)系數(shù)據(jù)庫,我們無須理解那么多復(fù)雜的事務(wù)模型,就可以直接使用。不過也正是因?yàn)檫@種簡單性,它的事務(wù)模型很不嚴(yán)格,這要求我們不能像使用關(guān)系數(shù)據(jù)庫的事務(wù)一樣來使用
Redis 事務(wù)的基本使用
每個(gè)事務(wù)的操作都有 begin、commit 和 rollback begin 指示事務(wù)的開始 commit 指示事務(wù)的提交 rollback 指示事務(wù)的回滾 它大致的形式如下 begin();
try
{command1();command2();....commit();
}
catch(Exception e)
{rollback();
} Redis 在形式上看起來也差不多,分別是 multi/exec/discard。multi 指示事務(wù)的開始,exec 指示事務(wù)的執(zhí)行,discard 指示事務(wù)的丟棄 > multi
OK
> incr books
QUEUED
> incr books
QUEUED
> exec
(integer) 1
(integer) 2 上面的指令演示了一個(gè)完整的事務(wù)過程,所有的指令在 exec 之前不執(zhí)行,而是緩存在服務(wù)器的一個(gè)事務(wù)隊(duì)列中,服務(wù)器一旦收到 exec 指令,才開執(zhí)行整個(gè)事務(wù)隊(duì)列,執(zhí)行完畢后一次性返回所有指令的運(yùn)行結(jié)果。因?yàn)?Redis 的單線程特性,它不用擔(dān)心自己在執(zhí)行隊(duì)列的時(shí)候被其它指令打攪,可以保證他們能得到的「原子性」執(zhí)行。QUEUED 是一個(gè)簡單字符串,同 OK 是一個(gè)形式,它表示指令已經(jīng)被服務(wù)器緩存到隊(duì)列里了 原子性 事務(wù)的原子性是指要么事務(wù)全部成功,要么全部失敗,那么 Redis 事務(wù)執(zhí)行是原子性的么? 下面我們來看一個(gè)特別的例子。 > multi
OK
> set books iamastring
QUEUED
> incr books
QUEUED
> set poorman iamdesperate
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
> get books
"iamastring"
> get poorman
"iamdesperate 上面的例子是事務(wù)執(zhí)行到中間遇到失敗了,因?yàn)槲覀儾荒軐?duì)一個(gè)字符串進(jìn)行數(shù)學(xué)運(yùn)算,事務(wù)在遇到指令執(zhí)行失敗后,后面的指令還繼續(xù)執(zhí)行,所以 poorman 的值能繼續(xù)得到設(shè)置 到這里,你應(yīng)該明白Redis 的事務(wù)根本不能算「原子性」,而僅僅是滿足了事務(wù)的「隔離性」,隔離性中的串行化——當(dāng)前執(zhí)行的事務(wù)有著不被其它事務(wù)打斷的權(quán)利 discard(丟棄) Redis 為事務(wù)提供了一個(gè) discard 指令,用于丟棄事務(wù)緩存隊(duì)列中的所有指令,在 exec 執(zhí)行之前 > get books
(nil)
> multi
OK
> incr books
QUEUED
> incr books
QUEUED
> discard
OK
> get books
(nil) 我們可以看到 discard 之后,隊(duì)列中的所有指令都沒執(zhí)行,就好像 multi 和 discard 中間的所有指令從未發(fā)生過一樣 優(yōu)化 上面的 Redis 事務(wù)在發(fā)送每個(gè)指令到事務(wù)緩存隊(duì)列時(shí)都要經(jīng)過一次網(wǎng)絡(luò)讀寫,當(dāng)一個(gè)事務(wù)內(nèi)部的指令較多時(shí),需要的網(wǎng)絡(luò) IO 時(shí)間也會(huì)線性增長。所以通常 Redis 的客戶端在執(zhí)行事務(wù)時(shí)都會(huì)結(jié)合 pipeline 一起使用,這樣可以將多次 IO 操作壓縮為單次 IO 操作。比如我們在使用 Python 的 Redis 客戶端時(shí)執(zhí)行事務(wù)時(shí)是要強(qiáng)制使用 pipeline 的 pipe = redis.pipeline(transaction=true)
pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute() Watch 考慮到一個(gè)業(yè)務(wù)場景,Redis 存儲(chǔ)了我們的賬戶余額數(shù)據(jù),它是一個(gè)整數(shù)?,F(xiàn)在有兩個(gè)并發(fā)的客戶端要對(duì)賬戶余額進(jìn)行修改操作,這個(gè)修改不是一個(gè)簡單的 incrby 指令,而是要對(duì)余額乘以一個(gè)倍數(shù)。Redis 可沒有提供 multiplyby 這樣的指令。我們需要先取出余額然后在內(nèi)存里乘以倍數(shù),再將結(jié)果寫回 Redis 這就會(huì)出現(xiàn)并發(fā)問題,因?yàn)橛卸鄠€(gè)客戶端會(huì)并發(fā)進(jìn)行操作。我們可以通過 Redis 的分布式鎖來避免沖突,這是一個(gè)很好的解決方案 但是分布式鎖是一種悲觀鎖,那是不是可以使用樂觀鎖的方式來解決沖突呢? Redis 提供了這種 watch 的機(jī)制,它就是一種樂觀鎖。有了 watch 我們又多了一種可以用來解決并發(fā)修改的方法。 watch 的使用方式如下 while True:do_watch()commands()multi()send_commands()try:exec()breakexcept WatchError:continue watch 會(huì)在事務(wù)開始之前盯住 1 個(gè)或多個(gè)關(guān)鍵變量,當(dāng)事務(wù)執(zhí)行時(shí),也就是服務(wù)器收到了 exec 指令要順序執(zhí)行緩存的事務(wù)隊(duì)列時(shí),Redis 會(huì)檢查關(guān)鍵變量自 watch 之后,是否被修改了 (包括當(dāng)前事務(wù)所在的客戶端)。如果關(guān)鍵變量被人動(dòng)過了,exec 指令就會(huì)返回 null 回復(fù)告知客戶端事務(wù)執(zhí)行失敗,這個(gè)時(shí)候客戶端一般會(huì)選擇重試 > watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事務(wù)執(zhí)行失敗
(nil) 當(dāng)服務(wù)器給 exec 指令返回一個(gè) null 回復(fù)時(shí),客戶端知道了事務(wù)執(zhí)行是失敗的,通??蛻舳?(redis-py) 都會(huì)拋出一個(gè) WatchError 這種錯(cuò)誤,不過也有些語言 (jedis) 不會(huì)拋出異常,而是通過在 exec 方法里返回一個(gè) null,這樣客戶端需要檢查一下返回結(jié)果是否為 null來確定事務(wù)是否執(zhí)行失敗 注意事項(xiàng) Redis 禁止在 multi 和 exec 之間執(zhí)行 watch 指令,而必須在 multi 之前做好盯住關(guān)鍵變量,否則會(huì)出錯(cuò) 接下來我們使用 Python 語言來實(shí)現(xiàn)對(duì)余額的加倍操作 import redis
def key_for(user_id):return "account_{}".format(user_id)
def double_account(client, user_id):key = key_for(user_id)while True:client.watch(key)value = int(client.get(key))value *= 2 # 加倍pipe = client.pipeline(transaction=True)pipe.multi()pipe.set(key, value)try:pipe.execute()break # 總算成功了except redis.WatchError:continue # 事務(wù)被打斷了,重試return int(client.get(key)) # 重新獲取余額
client = redis.StrictRedis()
user_id = "abc"
client.setnx(key_for(user_id), 5) # setnx 做初始化
print double_account(client, user_id) 下面我們再使用 Java 語言實(shí)現(xiàn)一遍 import java.util.List;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;
public class TransactionDemo {public static void main(String[] args) {Jedis jedis = new Jedis();String userId = "abc";String key = keyFor(userId);jedis.setnx(key, String.valueOf(5)); # setnx 做初始化System.out.println(doubleAccount(jedis, userId));jedis.close();}public static int doubleAccount(Jedis jedis, String userId) {String key = keyFor(userId);while (true) {jedis.watch(key);int value = Integer.parseInt(jedis.get(key));value *= 2; // 加倍Transaction tx = jedis.multi();tx.set(key, String.valueOf(value));List<Object> res = tx.exec();if (res != null) {break; // 成功了}}return Integer.parseInt(jedis.get(key)); // 重新獲取余額}public static String keyFor(String userId) {return String.format("account_{}", userId);}
} 我們常常聽說 Python 的代碼要比 Java 簡短太多,但是從這個(gè)例子中我們看到 Java的代碼比 python 的代碼也多不了多少,大約只多出 50% 思考題 為什么 Redis 的事務(wù)不能支持回滾?
總結(jié)
以上是生活随笔為你收集整理的redis事务原理,使用,详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 令牌桶限流之redis-cell的安装,
- 下一篇: goland设置Terminal