什么是分布式鎖?
要介紹分布式鎖,首先要提到與分布式鎖相對應的是線程鎖、進程鎖。
線程鎖:主要用來給方法、代碼塊加鎖。當某個方法或代碼使用鎖,在同一時刻僅有一個線程執行該方法或該代碼段。線程鎖只在同一JVM中有效果,因為線程鎖的實現在根本上是依靠線程之間共享內存實現的,比如synchronized是共享對象頭,顯示鎖Lock是共享某個變量(state)。
進程鎖:為了控制同一操作系統中多個進程訪問某個共享資源,因為進程具有獨立性,各個進程無法訪問其他進程的資源,因此無法通過synchronized等線程鎖實現進程鎖。
分布式鎖:當多個進程不在同一個系統中,用分布式鎖控制多個進程對資源的訪問。
分布式鎖的使用場景
線程間並發問題和進程間並發問題都是可以通過分布式鎖解決的,但是強烈不建議這樣做!因為采用分布式鎖解決這些小問題是非常消耗資源的!分布式鎖應該用來解決分布式情況下的多進程並發問題才是最合適的。
有這樣一個情境,線程A和線程B都共享某個變量X。
如果是單機情況下(單JVM),線程之間共享內存,只要使用線程鎖就可以解決並發問題。
如果是分布式情況下(多JVM),線程A和線程B很可能不是在同一JVM中,這樣線程鎖就無法起到作用了,這時候就要用到分布式鎖來解決。
分布式鎖簡介
其實Java世界的”半壁江山”——Spring早就提供了分布式鎖的實現。早期,分布式鎖的相關代碼存在於Spring Cloud的子項目Spring Cloud Cluster中,后來被遷到Spring Integration中。
可能有不少童鞋對Spring Integration不是很熟悉,簡單介紹一下——官方說法,這是一個 企業集成模式
的實現;通俗地說,Spring Integration的定位是一個輕量級的ESB,盡管它做了很多ESB不做的事情。順便說一下,Spring Cloud Stream的底層也是Spring Integration。
Spring Integration提供的全局鎖目前為如下存儲提供了實現:
- Gemfire
- JDBC
- Redis
- Zookeeper
它們使用相同的API抽象——這正是Spring最擅長的。這意味着,不論使用哪種存儲,你的編碼體驗是一樣的,有一天想更換實現,只需要修改依賴和配置就可以了,無需修改代碼。
編碼
新建一個sprinboot項目,然后配置相關內容和測試代碼。
1.pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.itmuch.cloud</groupId> <artifactId>redisLock</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>redisLock</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.0.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <optional>true</optional> <!-- 這個需要為 true 熱部署才有效 --> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-integration</artifactId> </dependency> <dependency> <groupId>org.springframework.integration</groupId> <artifactId>spring-integration-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <scope>test</scope> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
這里提一下,我之前都是用的1.5.9.RELEASE版本的springboot。但我發現添加了上面紅色字體的三個依賴之后,發現編譯不通過,發聵的信息是版本問題,后來我改為2.0.0.RELEASE版本,OK了。
2.application.yml
server:
port: 8080
spring:
redis:
port: 6379
host: localhost
當前這個應用的端口,我們設置為8080,然后Redis服務器的端口當然是默認的6379啦!
3.RedisLockConfiguration.java
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockRegistry(redisConnectionFactory, "spring-cloud");
}
}
這個很好理解,我們要用到鎖,當然得先注冊一個鎖的Bean對象到spring容器中,以便獲取使用。
4.testController.java
@RestController @RequestMapping(value = "index") public class testController { private final static Logger log = LoggerFactory.getLogger(testController.class); @Autowired private RedisLockRegistry redisLockRegistry; @GetMapping("test") public void test() throws InterruptedException { Lock lock = redisLockRegistry.obtain("lock"); boolean b1 = lock.tryLock(3, TimeUnit.SECONDS); log.info("b1 is : {}", b1); TimeUnit.SECONDS.sleep(5); boolean b2 = lock.tryLock(3, TimeUnit.SECONDS); log.info("b2 is : {}", b2); lock.unlock(); lock.unlock(); }
這個接口類中的代碼如果不太明白,不,不管你明不明白,都建議看一下org.springframework.integration.redis.util.RedisLockRegistry這個類的注釋。
/** * Implementation of {@link LockRegistry} providing a distributed lock using Redis. * Locks are stored under the key {@code registryKey:lockKey}. Locks expire after * (default 60) seconds. Threads unlocking an * expired lock will get an {@link IllegalStateException}. This should be * considered as a critical error because it is possible the protected * resources were compromised. * <p> * Locks are reentrant. * <p> * <b>However, locks are scoped by the registry; a lock from a different registry with the * same key (even if the registry uses the same 'registryKey') are different * locks, and the second cannot be acquired by the same thread while the first is * locked.</b> * <p> * <b>Note: This is not intended for low latency applications.</b> It is intended * for resource locking across multiple JVMs. * <p> * {@link Condition}s are not supported.
測試
這樣,我們一個工程就開發完了,你可以再復制一份工程,RedisLock2.只要把端口號改了就行,比如改為8081.然后同時啟動倆工程。
我一般都是一個放在eclipse中跑,一個在終端通過命令行啟動,這樣簡潔一點。
接下來,你打開兩個網頁,輸好地址,然后快速依次訪問兩個工程的接口:
http://localhost:8081/index/test
http://localhost:8080/index/test
然后看兩個控制台的結果
先看第一個:兩個都是true,說明同一個線程是可以獲得到鎖的,正如上面注釋
Locks are reentrant.
再看第二個:第一次是false,因為上一個線程鎖住了,還沒有釋放,所以它是獲取不到的。而第二次返回true,說明獲得到了,因為第一個工程中跑的線程已經釋放了鎖。
另外,你如果要看到現象,你開啟RedisClient.你訪問其中一個工程的接口,快速刷新redis對應的db(我的是db0)
你不停的刷新db0,你會看到這個spring-cloud的鎖key,過幾秒就消失了,因為被釋放了嘛。這也證實了我們的運行結果。
如果你代碼中不釋放鎖,那么這個spring-cloud的鎖key過60秒會自動消失,正如上面注釋所描述的那樣。
代碼下載地址:https://gitee.com/fengyuduke/my_open_resources/blob/master/redisLock.zip