最近在開發中涉及到了多個客戶端的對redis的某個key同時進行增刪的問題。這裏就會涉及一個問題:鎖java
redis中存放了某個用戶的帳戶餘額 ,例如100 (用戶id:餘額)git
A端須要對用戶扣費-1,須要兩步:github
A1.將該用戶的目前餘額取出來(100)redis
A2.將餘額扣除一部分(99)後再插入到redis中算法
B端須要對用戶充值+10,須要兩步:spring
B1.將該用戶的目前餘額取出來(99)數據庫
B2.將餘額添加充值額度(109)後再插入到redis中api
咱們的指望執行順序是A一、A二、B一、B2 結果就會是109服務器
可是若是不加鎖,就會出現A一、B一、A二、B2(110)或者其餘各類隨機狀況,這樣就會形成數據錯誤。mybatis
以前參考的不少博客,關於redis加鎖都是先setNX()獲取鎖,而後再setExpire()設置鎖的有效時間。
然而這樣的話獲取鎖的操做就不是原子性的了,若是setNX後系統宕機,就會形成鎖死,系統阻塞。
根據官方的推薦(https://redis.io/topics/distlock),最好使用set命令:SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX PX設置有效時間 NX屬性的做用就是若是key存在就返回失敗,不然插入數據。
須要注意的是:
在Redis 2.6.12以前,set只能返回OK,因此沒法判斷操做是否成功,因此也就不適用。
若是使用的是spring-boot-starter-data-redis依賴,那麼在2.x版本以前的接口也不支持上述的set操做
java代碼:
//獲取鎖 //鎖的鍵值須要具備標誌性。 //例如,如今有兩個系統須要對key=user_id,value=user_balance進行操做,這時就能夠設計這個鍵的鎖爲user_id+"_key" String user_id="1"; String key=user_id+"_key"; //值設置爲一個隨機數(下面講緣由) String random_value=UUID.randomUUID().toString(); redisTemplate.execute((RedisCallback<Boolean>) (RedisConnection connection)->{ //只有2.0以上的版本才支持set返回插入結果Boolean //此命令的意思是隻有key不存在,才插入值,而且設置有效時間爲10s connection.set(key.getBytes(), random_value.getBytes(), Expiration.seconds(10), SetOption.SET_IF_ABSENT); //本示例因爲依賴版本低於2.0,因此沒法接受set設置結果 Boolean result=true; return result; }); //進行更新操做... //釋放鎖 //爲何釋放以前要比較一下? //這是爲了防止刪除掉別人的鎖,例如此場景中:若是咱們的中間操做超過了10s那麼鎖會自動釋放,這時別人會再獲取鎖。 //若是咱們執行完中間就直接刪除鎖的話,就會把別人的鎖刪除 if(redisTemplate.opsForValue().get(key)==random_value) { redisTemplate.delete(key); }
能夠發現,若是本身來實現的話,受限不少。而且這仍是最基本的操做,包括出錯重試等功能都沒有。
因此咱們要學習redis推薦的reids工具redisson
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.6.1</version> </dependency>
二.在resources文件夾添加配置文件redisson.yml
singleServerConfig: #鏈接空閒超時,單位:毫秒 idleConnectionTimeout: 10000 pingTimeout: 1000 #鏈接超時,單位:毫秒 connectTimeout: 10000 #命令等待超時,單位:毫秒 timeout: 3000 #命令失敗重試次數 retryAttempts: 3 #命令重試發送時間間隔,單位:毫秒 retryInterval: 1500 #從新鏈接時間間隔,單位:毫秒 reconnectionTimeout: 3000 #執行失敗最大次數 failedAttempts: 3 #單個鏈接最大訂閱數量 subscriptionsPerConnection: 5 #客戶端名稱 clientName: null #地址 address: "redis://192.168.1.16:6379" #數據庫編號 database: 0 #密碼 password: xiaokong #發佈和訂閱鏈接的最小空閒鏈接數 subscriptionConnectionMinimumIdleSize: 1 #發佈和訂閱鏈接池大小 subscriptionConnectionPoolSize: 50 #最小空閒鏈接數 connectionMinimumIdleSize: 32 #鏈接池大小 connectionPoolSize: 64 #是否啓用DNS監測 dnsMonitoring: false #DNS監測時間間隔,單位:毫秒 dnsMonitoringInterval: 5000 threads: 0 nettyThreads: 0 codec: !<org.redisson.codec.JsonJacksonCodec> {} transportMode : "NIO"
三.在Application中設置RedissonClient
import org.mybatis.spring.annotation.MapperScan; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement @MapperScan("com.xxx.mapper") public class Application { public static void main(String [] args) { SpringApplication.run(Application.class, args); } @Bean(destroyMethod="shutdown") public RedissonClient redisson() throws IOException { RedissonClient redisson = Redisson.create( Config.fromYAML(new ClassPathResource("redisson.yml").getInputStream())); return redisson; } }
四.在代碼中使用
@Autowired private RedissonClient redisson; @Test public void redisson() { String user_id="1"; String key=user_id+"_key"; //獲取鎖 RLock lock = redisson.getLock(key); lock.lock(); //執行具體邏輯... RBucket<Object> bucket = redisson.getBucket("a"); bucket.set("bb"); lock.unlock(); }
須要注意的是redisson的使用和redisTemplate有比較大的區別,這裏簡單介紹一下幾個特性:(剛用時迷了好久,但願你們能少走些彎路)
1.在redisson中不須要set指令,舉個例子:
RBucket<Object> bucket = redisson.getBucket("a");
bucket.set("bb");
在這兩條語句中,咱們只獲取了key="a"的bucket類型對象(裏面能夠裝一個任意對象)。而後修改bucket裏面一個值,其實這時["a","bb"]已經被存入redis了
2.全部的值都是結構體
和上例的RBucket結構體同樣,redisson提供了十幾種結構體(https://github.com/redisson/redisson/wiki/7.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%9B%86%E5%90%88)供咱們使用,當取值時,redisson也會自動將值轉換成對應的結構體。因此若是使用redisson取redisTemplate放入的值,就要當心報錯
這種我尚未具體實現過,爲何會出現這種算法,主要是應對redis服務器宕機的問題。當redis宕機時,即便有主從,可是依然會有一個同步間隔。這樣就會形成數據流失。
固然,更爲嚴重的是,在分佈式狀況下,丟失的是鎖,咱們知道通常用鎖的數據都是比較重要的。
一個場景:A在向主機1請求到鎖成功後,主機1宕機了。如今從機1a變成了主機。可是數據沒有同步,從機1a是沒有A的鎖的。那麼B又能夠得到一個鎖。這樣就會形成數據錯誤。
redlock主要思想就是作數據冗餘。創建5臺獨立的集羣,當咱們發送一個數據的時候,要保證3臺(n/2+1)以上的機器接受成功纔算成功,不然重試或報錯。
固然具體是很複雜,想研究的能夠看看(https://redis.io/topics/distlock)
就像以前討論的,方式2只能保證客戶端的正確,卻沒法保證服務端的宕機數據丟失。方式三的數據完整性很高,可是管理起來很複雜。這時就有了一個折中的作法:
將鎖存放在zookeeper中,因爲zookeeper與redis的場景不一樣,因此zookeeper的算法對數據的完整性要求很高。在分佈式的zookeeper中,數據是很難丟失的。
這樣,咱們就能夠把鎖放到zookeeper中,來保證鎖的完整性。
好吧,這個我也沒有實驗過(羞恥),不過網上又不少這方面的博客,之後用到再說吧~~~通常項目用方式二就能夠啦,