Java都為我們提供了各種鎖,為什么還需要分布式鎖?
目前的項(xiàng)目單體結(jié)構(gòu)的基本上已經(jīng)沒有了,大多是分布式集群或者是微服務(wù)這些。既然是多臺服務(wù)器。就免不了資源的共享問題。既然是資源共享就免不了并發(fā)的問題。針對這些問題,redis也給出了一個很好的解決方案,那就是分布式鎖。這篇文章主要是針對為什么需要使用分布式鎖這個話題來展開討論的。
前一段時間在群里有個兄弟問,既然分布式鎖能解決大部分生產(chǎn)問題,那么java為我們提供的那些鎖有什么用呢?直接使用分布式鎖不就結(jié)了嘛。針對這個問題我想了很多,一開始是在網(wǎng)上找找看看有沒有類似的回答。后來想了想。想要解決這個問題,還需要從本質(zhì)上來分析。
OK,開始上車出發(fā)。
一、前言
既然是分布式鎖,這就說明服務(wù)器不是一臺,可能是很多臺。我們使用一個案例,來一步一步說明。假設(shè)某網(wǎng)站有一個秒殺商品,一看還有100件,于是陜西、江蘇、西藏等地的人都看到了這個活動,于是開始進(jìn)行瘋狂秒殺。假設(shè)這個秒殺商品的數(shù)量值保存在一個redis數(shù)據(jù)庫中。
但是不同地區(qū)的用戶使用不同的服務(wù)器進(jìn)行秒殺。這樣就形成了一個集群訪問的方式。
方式我們使用Springboot來整合redis。
二、項(xiàng)目搭建準(zhǔn)備
(1)添加pom依賴
- <dependency>
 - <groupId>org.springframework.boot</groupId>
 - <artifactId>spring-boot-starter-web</artifactId>
 - </dependency>
 - <dependency>
 - <groupId>org.springframework.boot</groupId>
 - <artifactId>spring-boot-starter-test</artifactId>
 - <scope>test</scope>
 - </dependency>
 - <dependency>
 - <groupId>org.springframework.boot</groupId>
 - <artifactId>spring-boot-starter-data-redis</artifactId>
 - </dependency>
 - <dependency>
 - <groupId>org.apache.commons</groupId>
 - <artifactId>commons-pool2</artifactId>
 - </dependency>
 
(2)添加屬性配置
- # Redis數(shù)據(jù)庫索引(默認(rèn)為0)
 - spring.redis.database=0
 - # Redis服務(wù)器地址
 - spring.redis.host=localhost
 - # Redis服務(wù)器連接端口
 - spring.redis.port=6379
 - # Redis服務(wù)器連接密碼(默認(rèn)為空)
 - spring.redis.password=
 - # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制) 默認(rèn) 8
 - spring.redis.lettuce.pool.max-active=8
 - # 連接池最大阻塞等待時間(使用負(fù)值表示沒有限制) 默認(rèn) -1
 - spring.redis.lettuce.pool.max-wait=-1
 - # 連接池中的最大空閑連接 默認(rèn) 8
 - spring.redis.lettuce.pool.max-idle=8
 - # 連接池中的最小空閑連接 默認(rèn) 0
 - spring.redis.lettuce.pool.min-idle=0
 
(3)新建config包,創(chuàng)建RedisConfig類
- @Configuration
 - public class RedisConfig {
 - @Bean
 - public RedisTemplate<String, Serializable>
 - redisTemplate(LettuceConnectionFactory connectionFactory) {
 - RedisTemplate<String, Serializable> redisTemplate = new RedisTemplate<>();
 - redisTemplate.setKeySerializer(new StringRedisSerializer());
 - redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
 - redisTemplate.setConnectionFactory(connectionFactory);
 - return redisTemplate;
 - }
 - }
 
(4)新建controller,創(chuàng)建Mycontroller類
- @RestController
 - public class MyController {
 - @Autowired
 - private StringRedisTemplate stringRedisTemplate;
 - @GetMapping("/test")
 - public String deduceGoods(){
 - int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
 - int realGoods = goods-1;
 - if(goods>0){
 - stringRedisTemplate.opsForValue().set("goods",realGoods+"");
 - return "你已經(jīng)成功秒殺商品,此時還剩余:" + realGoods + "件";
 - }else{
 - return "商品已經(jīng)售罄,歡迎下次活動";
 - }
 - }
 - }
 
很簡單的一個整合教程。這個端口是8080,我們復(fù)制一份這個項(xiàng)目,把端口改成8090,并且以nginx作負(fù)載均衡搭建集群。現(xiàn)在環(huán)境我們已經(jīng)整理好了。下面我們就開始進(jìn)行分析。
三、為什么需要分布式鎖
階段一:采用原生方式
我們使用多個線程訪問8080這個端口。因?yàn)闆]有加鎖,此時肯定會出現(xiàn)并發(fā)問題。因此我們可能會想到,既然這個goods是一個共享資源,而且是多線程訪問的,就立馬能想到j(luò)ava中的各種鎖了,最有名的就是synchronized。所以我們不如對上面的代碼進(jìn)行優(yōu)化。
階段二:使用synchronized加鎖
此時我們對代碼修改一下:
- @RestController
 - public class MyController {
 - @Autowired
 - private StringRedisTemplate stringRedisTemplate;
 - @GetMapping("/test")
 - public String deduceGoods(){
 - synchronized (this){
 - int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
 - int realGoods = goods-1;
 - if(goods>0){
 - stringRedisTemplate.opsForValue().set("goods",realGoods+"");
 - return "你已經(jīng)成功秒殺商品,此時還剩余:" + realGoods + "件";
 - }else{
 - return "商品已經(jīng)售罄,歡迎下次活動";
 - }
 - }
 - }
 - }
 
看到?jīng)],現(xiàn)在我們使用synchronized關(guān)鍵字加上鎖,這樣多個線程并發(fā)訪問的時候就不會出現(xiàn)數(shù)據(jù)不一致等各種問題了。這種方式在單體結(jié)構(gòu)下的確有用。目前的項(xiàng)目單體結(jié)構(gòu)的很少,一般都是集群方式的。此時的synchronized就不再起作用了。為什么synchronized不起作用了呢?
我們采用集群的方式去訪問秒殺商品(nginx為我們做了負(fù)載均衡)。就會看到數(shù)據(jù)不一致的現(xiàn)象。也就是說synchronized關(guān)鍵字的作用域其實(shí)是一個進(jìn)程,在這個進(jìn)程下面的所有線程都能夠進(jìn)行加鎖。但是多進(jìn)程就不行了。對于秒殺商品來說,這個值是固定的。但是每個地區(qū)都可能有一臺服務(wù)器。這樣不同地區(qū)服務(wù)器不一樣,地址不一樣,進(jìn)程也不一樣。因此synchronized無法保證數(shù)據(jù)的一致性。
階段三:分布式鎖
上面synchronized關(guān)鍵字無法保證多進(jìn)程的鎖機(jī)制,為了解決這個問題,我們可以使用redis分布式鎖。現(xiàn)在我們把代碼再進(jìn)行修改一下:
- @RestController
 - public class MyController {
 - @Autowired
 - private StringRedisTemplate stringRedisTemplate;
 - @GetMapping("/test")
 - public String deduceGoods(){
 - Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","馮冬冬");
 - if(!result){
 - return "其他人正在秒殺,無法進(jìn)入";
 - }
 - int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
 - int realGoods = goods-1;
 - if(goods>0){
 - stringRedisTemplate.opsForValue().set("goods",realGoods+"");
 - System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realGoods + "件");
 - }else{
 - System.out.println("商品已經(jīng)售罄,歡迎下次活動");
 - }
 - stringRedisTemplate.delete("lock");
 - return "success";
 - }
 - }
 
就是這么簡單,我們只是加了一句話,然后進(jìn)行判斷了一下。其實(shí)setIfAbsent方法的作用就是redis中的setnx。意思是如果當(dāng)前key已經(jīng)存在了,就不做任何操作了,返回false。如果當(dāng)前key不存在,那我們就可以操作。最后別忘了釋放這個key,這樣別人就可以再進(jìn)來實(shí)時秒殺操作。
當(dāng)然這里只是給出一個最基本的案例,其實(shí)分布式鎖實(shí)現(xiàn)起來步驟還是比較多的,而且里面很多坑也沒有給出。我們隨便解決幾個:
階段四:分布式鎖優(yōu)化
(1)第一個坑:秒殺商品出現(xiàn)異常,最終無法釋放lock分布式鎖
- public String deduceGoods() throws Exception{
 - Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","馮冬冬");
 - if(!result){
 - return "其他人正在秒殺,無法進(jìn)入";
 - }
 - try {
 - int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
 - int realGoods = goods-1;
 - if(goods>0){
 - stringRedisTemplate.opsForValue().set("goods",realGoods+"");
 - System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realGoods + "件");
 - }else{
 - System.out.println("商品已經(jīng)售罄,歡迎下次活動");
 - }
 - }finally {
 - stringRedisTemplate.delete("lock");
 - }
 - return "success";
 - }
 
此時我們加一個try和finally語句就可以了。最終一定要刪除lock。
(2)第二個坑:秒殺商品時間太久,其他用戶等不及
- public String deduceGoods() throws Exception{
 - stringRedisTemplate.expire("lock",10, TimeUnit.MILLISECONDS);
 - Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock","馮冬冬");
 - if(!result){
 - return "其他人正在秒殺,無法進(jìn)入";
 - }
 - try {
 - int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
 - int realGoods = goods-1;
 - if(goods>0){
 - stringRedisTemplate.opsForValue().set("goods",realGoods+"");
 - System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realGoods + "件");
 - }else{
 - System.out.println("商品已經(jīng)售罄,歡迎下次活動");
 - }
 - }finally {
 - stringRedisTemplate.delete("lock");
 - }
 - return "success";
 - }
 
給其添加一個過期時間,也就是說如果10毫秒內(nèi)沒有秒殺成功,就表示秒殺失敗,換下一個用戶。
(3)第三個坑:高并發(fā)場景下,秒殺時間太久,鎖永久失效問題
我們剛剛設(shè)置的鎖過期時間是10毫秒,如果一個用戶秒殺時間是15毫秒,這也就意味著他可能還沒秒殺成功,就有其他用戶進(jìn)來了。當(dāng)這種情況過多時,就可能有大量用戶還沒秒殺成功其他大量用戶就進(jìn)來了。有可能其他用戶提前刪除了lock,但是當(dāng)前用戶還沒有秒殺成功。最終造成數(shù)據(jù)的不一致??纯慈绾谓鉀Q:
- public String deduceGoods() throws Exception{
 - String user = UUID.randomUUID().toString();
 - stringRedisTemplate.expire("lock",10, TimeUnit.MILLISECONDS);
 - Boolean result = stringRedisTemplate.opsForValue().setIfAbsent("lock",user);
 - if(!result){
 - return "其他人正在秒殺,無法進(jìn)入";
 - }
 - try {
 - int goods =Integer.parseInt(stringRedisTemplate.opsForValue().get("goods"));
 - int realGoods = goods-1;
 - if(goods>0){
 - stringRedisTemplate.opsForValue().set("goods",realGoods+"");
 - System.out.println("你已經(jīng)成功秒殺商品,此時還剩余:" + realGoods + "件");
 - }else{
 - System.out.println("商品已經(jīng)售罄,歡迎下次活動");
 - }
 - }finally {
 - if(user.equals(stringRedisTemplate.opsForValue().get("lock"))){
 - stringRedisTemplate.delete("lock");
 - }
 - }
 - return "success";
 - }
 
也就是說,我們在刪除lock的時候判斷是不是當(dāng)前的線程,如果是那就刪除,如果不是那就不刪除,這樣就算別的線程進(jìn)來也不會亂刪lock,造成混亂。
OK,到目前為止基本上把分布式鎖的緣由介紹了一遍。對于分布式鎖redisson完成的相當(dāng)出色,下篇文章也將圍著繞Redisson來介紹一下分布式如何實(shí)現(xiàn),以及其中的原理。
本文轉(zhuǎn)載自微信公眾號「愚公要移山」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系愚公要移山公眾號。


















 
 
 











 
 
 
 