Redis-09Redis的基础事务
文章目錄
- 概述
- Redis 事務命令
- Redis 的基礎事務
- 在 Spring 中使用 Redis 事務命令
- 代碼
概述
和其他大部分的 NoSQL 不同,Redis 是存在事務的,盡管它沒有數據庫那么強大,但是它還是很有用的,尤其是在那些需要高并發的網站當中 ,使用 Redis 讀/寫數據要比數據庫快得多,如果使用 Redis 事務在某種場合下去替代數據庫事務,則可以在保證數據一致性的同時,大幅度提高數據讀/寫 的響應速度。
互聯網系統很多用戶同時訪問服務器的可能性很大,尤其在一些商品搶購、搶紅包等場合,對性能和數據的一致性有著很高的要求,而存儲系統的讀/寫響應速度對于這類場景的性能的提高是十分重要的 。
在 Redis 中,也存在多個客戶端同時向 Redis 系統發送命令的并發可能性,因此同一個數據,可能在不同的時刻被不同的線程所操縱,這樣就出現了并發下的數據一致的問題。為了保證異性數據的安全性, Redis 為提供了事務方案。而 Redis 的事務是使用 MULTI-EXEC的命令組合,使用它可以提供兩個重要的保證 :
在一個 Redis 的連接中,請注意要求是一個連接,所以更多的時候在使用 Spring 中會使用 SessionCallback 接口進行處理,在 Redis 中使用事務會經過 3 個過程
Redis 事務命令
官網: https://redis.io/commands#transactions
| multi | 開啟事務命令,之后的命令就進入隊列,而不會馬上被執行 | 在事務生存期間,所有的 Redis 關于數據結構的命令都會入隊 |
| watch key1 [key2 …] | 監聽某些鍵,當被監聽的鍵在事務執行前被修改,則事務會被回滾 | 使用樂觀鎖 |
| unwatch key1 [key2 …] | 取消監聽某些鍵 | ----- |
| exec | 執行事務,如果被監聽的鍵沒有被修改,則采用執行命令,否則就回滾命令 | 在執行事務隊列存儲的命令前, Redis 會檢測被監聽的鍵值對有沒有發生變化,如果沒有則執行命令 ,否則就回滾事務 |
| discard | 回滾事務 | 回滾進入隊列的事務命令,之后就不能再用 exec命令提交了 |
Redis 的基礎事務
在 Redis 中開啟事務是 multi 命令,而執行事務是 exec 命令。 multi 到 exec 命令之間的Redis 命令將采取進入隊列的形式,直至 exec 命令的出現,才會一次性發送隊列里的命令去執行,而在執行這些命令的時候其他客戶端就不能再插入任何命令了,這就是 Redis 的事務機制。
[redis@artisan bin]$ ./redis-cli 127.0.0.1:6379> auth artisan OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set key1 value1 QUEUED 127.0.0.1:6379> get key1 QUEUED 127.0.0.1:6379> exec 1) OK 2) "value1" 127.0.0.1:6379>從上述命令中可以看到,先使用 multi 啟動了 Redis 的事務,因此進入了 set 和 get 命令,我們可以發現它并未馬上執行,而是返回了 一個飛回歸D”的結果。這說明 Redis 將其放入隊列中,并不會馬上執行,當命令執行到 exec 的時候它就會把隊列中的命令發送給Redis 服務器 , 這樣存儲在隊列中的命令就會被執行了,所以才會"OK"和"value1"的輸出返回 。
如果回滾事務,則可以使用 discard 命令,它就會進入在事務隊列中的命令,這樣事務中的方法就不會被執行了,使用 discard 命令取消事務如下所示
127.0.0.1:6379> MULTI OK 127.0.0.1:6379> SET key2 value2 QUEUED 127.0.0.1:6379> GET key2 QUEUED 127.0.0.1:6379> DISCARD OK 127.0.0.1:6379> EXEC (error) ERR EXEC without MULTI 127.0.0.1:6379>當使用了 discard 命令后 ,再使用 exec 命令時就會報錯,因為 discard 命令已經取消了事務中的命令,而到了 exec 命令時,隊列里面己經沒有命令可以執行了,所以就出現了報錯的情況。
在 Spring 中使用 Redis 事務命令
在 Spring 中要使用同一個連接操作 Redis 命令的場景,這個時候我們借助的是 Spring 提供的 SessionCallback 接口,采用 Spring 去實現上述的命令.
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><context:property-placeholder location="classpath:redis/redis.properties" /><!--2,注意新版本2.3以后,JedisPoolConfig的property name,不是maxActive而是maxTotal,而且沒有maxWait屬性,建議看一下Jedis源碼或百度。 --><!-- redis連接池配置 --><bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"><!--最大空閑數 --><property name="maxIdle" value="${redis.maxIdle}" /><!--連接池的最大數據庫連接數 --><property name="maxTotal" value="${redis.maxTotal}" /><!--最大建立連接等待時間 --><property name="maxWaitMillis" value="${redis.maxWaitMillis}" /><!--逐出連接的最小空閑時間 默認1800000毫秒(30分鐘) --><property name="minEvictableIdleTimeMillis" value="${redis.minEvictableIdleTimeMillis}" /><!--每次逐出檢查時 逐出的最大數目 如果為負數就是 : 1/abs(n), 默認3 --><property name="numTestsPerEvictionRun" value="${redis.numTestsPerEvictionRun}" /><!--逐出掃描的時間間隔(毫秒) 如果為負數,則不運行逐出線程, 默認-1 --><property name="timeBetweenEvictionRunsMillis" value="${redis.timeBetweenEvictionRunsMillis}" /><property name="testOnBorrow" value="true"></property><property name="testOnReturn" value="true"></property><property name="testWhileIdle" value="true"></property></bean><!--redis連接工廠 --><bean id="jedisConnectionFactory"class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"destroy-method="destroy"><property name="poolConfig" ref="jedisPoolConfig"></property><!--IP地址 --><property name="hostName" value="${redis.host.ip}"></property><!--端口號 --><property name="port" value="${redis.port}"></property><!--如果Redis設置有密碼 --><property name="password" value="${redis.password}" /> <!--客戶端超時時間單位是毫秒 --><property name="timeout" value="${redis.timeout}"></property><property name="usePool" value="true" /><!--<property name="database" value="0" /> --></bean><!-- 鍵值序列化器設置為String 類型 --><bean id="stringRedisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/><!-- redis template definition --><bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"p:connection-factory-ref="jedisConnectionFactory"p:keySerializer-ref="stringRedisSerializer"p:valueSerializer-ref="stringRedisSerializer"></bean></beans> package com.artisan.redis.transaction;import java.util.List;import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.SessionCallback;public class SpringRedisTransaction {@SuppressWarnings({ "unchecked", "rawtypes", "resource" })public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:spring/spring-redis-string.xml");RedisTemplate<String, String> redisTemplate = ctx.getBean(RedisTemplate.class);// 清掉keyredisTemplate.delete("key1");SessionCallback sessionCallback = (SessionCallback) (RedisOperations ops) -> {// 開啟事務ops.multi();// 設置值ops.boundValueOps("key1").set("artisan");// 注意由于命令只是進入隊列 ,而沒有被執行,所以此處采用 get 命令 ,而 value 卻返回為nullString value = (String) ops.boundValueOps("key1").get();System.out.println("事務執行過程中 , 命令入隊列,而沒有被執行,所以 value 為空 :value=" + value);// 此時 list 會保存之前進入隊列的所有命令的結果List list = ops.exec();for (int i = 0; i < list.size(); i++) {System.out.println("隊列中的命令返回的結果:" + list.get(i).toString());}// 事務結束后 , 獲取 valuelvalue = (String) redisTemplate.opsForValue().get("key1");System.out.println("----:" + value);return value;};// 執行Redis命令String value = (String) redisTemplate.execute(sessionCallback);System.out.println("value:" + value);}}執行結果
INFO : org.springframework.context.support.ClassPathXmlApplicationContext - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@73a8dfcc: startup date [Thu Sep 27 12:10:45 CST 2018]; root of context hierarchy INFO : org.springframework.beans.factory.xml.XmlBeanDefinitionReader - Loading XML bean definitions from class path resource [spring/spring-redis-string.xml] 事務執行過程中 , 命令入隊列,而沒有被執行,所以 value 為空 :value=null 隊列中的命令返回的結果:true 隊列中的命令返回的結果:artisan ----:artisan value:artisan采用了 Lambda 表達式( Java 8 以后才引入 Lambda 表達式)來為 SessionCallBack 接口 實現了業務邏輯.
從代碼看,使用了 SessionCallBack 接口,從而保證所有的命令都是通過同一個 Redis 的連接進行操作的。
在使用 multi 命令后 , 要特別注意的是,使用 get 等返回值的方法一律返回為空 ,因為在 Redis 中它只是把命令緩存到隊列中,而沒有去執行 。
使用 exec 后就會執行事務,行行完了事務后,執行 get 命令就能正常返回結果了。
最后使用 redisTemplate.execute(callBack);就能執行我們在 SessionCallBack 接口定義Lambda 表達式的業務邏輯,并將獲得其返回值。
需要再強調的是 : 這里打印出來的 value=null,是因為在事務中,所有的方法都只會被
緩存到 Redis 事務隊列中,而沒有立即執行,所以返回為 null,
如果我們希望得到 Redis 執行事務各個命令的結果,可以用這行代碼 :
List list = ops.exec();這段代碼將返回之前在事務隊列中所有命令的執行結果,并保存在一個 List 中,只要在 SessionCallback 接口的 execute 方法中將 list 返回,就可以在程序中獲得各個命令執行的結果了 .
代碼
代碼托管到了 https://github.com/yangshangwei/redis_learn
《新程序員》:云原生和全面數字化實踐50位技術專家共同創作,文字、視頻、音頻交互閱讀總結
以上是生活随笔為你收集整理的Redis-09Redis的基础事务的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis-08Redis数据结构--基
- 下一篇: Redis-10Redis的事务回滚