Java都為我們提供了各種鎖,為什么還需要分布式鎖?
目前的項(xiàng)目單體結(jié)構(gòu)的基本上已經(jīng)沒有了,大多是分布式集群或者是微服務(wù)這些。既然是多臺(tái)服務(wù)器。就免不了資源的共享問題。既然是資源共享就免不了并發(fā)的問題。針對(duì)這些問題,redis也給出了一個(gè)很好的解決方案,那就是分布式鎖。這篇文章主要是針對(duì)為什么需要使用分布式鎖這個(gè)話題來展開討論的。
前一段時(shí)間在群里有個(gè)兄弟問,既然分布式鎖能解決大部分生產(chǎn)問題,那么java為我們提供的那些鎖有什么用呢?直接使用分布式鎖不就結(jié)了嘛。針對(duì)這個(gè)問題我想了很多,一開始是在網(wǎng)上找找看看有沒有類似的回答。后來想了想。想要解決這個(gè)問題,還需要從本質(zhì)上來分析。
OK,開始上車出發(fā)。
一、前言
既然是分布式鎖,這就說明服務(wù)器不是一臺(tái),可能是很多臺(tái)。我們使用一個(gè)案例,來一步一步說明。假設(shè)某網(wǎng)站有一個(gè)秒殺商品,一看還有100件,于是陜西、江蘇、西藏等地的人都看到了這個(gè)活動(dòng),于是開始進(jìn)行瘋狂秒殺。假設(shè)這個(gè)秒殺商品的數(shù)量值保存在一個(gè)redis數(shù)據(jù)庫(kù)中。
但是不同地區(qū)的用戶使用不同的服務(wù)器進(jìn)行秒殺。這樣就形成了一個(gè)集群訪問的方式。
方式我們使用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ù)庫(kù)索引(默認(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
- # 連接池最大阻塞等待時(shí)間(使用負(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)成功秒殺商品,此時(shí)還剩余:" + realGoods + "件";
- }else{
- return "商品已經(jīng)售罄,歡迎下次活動(dòng)";
- }
- }
- }
很簡(jiǎn)單的一個(gè)整合教程。這個(gè)端口是8080,我們復(fù)制一份這個(gè)項(xiàng)目,把端口改成8090,并且以nginx作負(fù)載均衡搭建集群?,F(xiàn)在環(huán)境我們已經(jīng)整理好了。下面我們就開始進(jìn)行分析。
三、為什么需要分布式鎖
階段一:采用原生方式
我們使用多個(gè)線程訪問8080這個(gè)端口。因?yàn)闆]有加鎖,此時(shí)肯定會(huì)出現(xiàn)并發(fā)問題。因此我們可能會(huì)想到,既然這個(gè)goods是一個(gè)共享資源,而且是多線程訪問的,就立馬能想到j(luò)ava中的各種鎖了,最有名的就是synchronized。所以我們不如對(duì)上面的代碼進(jìn)行優(yōu)化。
階段二:使用synchronized加鎖
此時(shí)我們對(duì)代碼修改一下:
- @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)成功秒殺商品,此時(shí)還剩余:" + realGoods + "件";
- }else{
- return "商品已經(jīng)售罄,歡迎下次活動(dòng)";
- }
- }
- }
- }
看到?jīng)],現(xiàn)在我們使用synchronized關(guān)鍵字加上鎖,這樣多個(gè)線程并發(fā)訪問的時(shí)候就不會(huì)出現(xiàn)數(shù)據(jù)不一致等各種問題了。這種方式在單體結(jié)構(gòu)下的確有用。目前的項(xiàng)目單體結(jié)構(gòu)的很少,一般都是集群方式的。此時(shí)的synchronized就不再起作用了。為什么synchronized不起作用了呢?
我們采用集群的方式去訪問秒殺商品(nginx為我們做了負(fù)載均衡)。就會(huì)看到數(shù)據(jù)不一致的現(xiàn)象。也就是說synchronized關(guān)鍵字的作用域其實(shí)是一個(gè)進(jìn)程,在這個(gè)進(jìn)程下面的所有線程都能夠進(jìn)行加鎖。但是多進(jìn)程就不行了。對(duì)于秒殺商品來說,這個(gè)值是固定的。但是每個(gè)地區(qū)都可能有一臺(tái)服務(wù)器。這樣不同地區(qū)服務(wù)器不一樣,地址不一樣,進(jìn)程也不一樣。因此synchronized無法保證數(shù)據(jù)的一致性。
階段三:分布式鎖
上面synchronized關(guān)鍵字無法保證多進(jìn)程的鎖機(jī)制,為了解決這個(gè)問題,我們可以使用redis分布式鎖?,F(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)成功秒殺商品,此時(shí)還剩余:" + realGoods + "件");
- }else{
- System.out.println("商品已經(jīng)售罄,歡迎下次活動(dòng)");
- }
- stringRedisTemplate.delete("lock");
- return "success";
- }
- }
就是這么簡(jiǎn)單,我們只是加了一句話,然后進(jìn)行判斷了一下。其實(shí)setIfAbsent方法的作用就是redis中的setnx。意思是如果當(dāng)前key已經(jīng)存在了,就不做任何操作了,返回false。如果當(dāng)前key不存在,那我們就可以操作。最后別忘了釋放這個(gè)key,這樣別人就可以再進(jìn)來實(shí)時(shí)秒殺操作。
當(dāng)然這里只是給出一個(gè)最基本的案例,其實(shí)分布式鎖實(shí)現(xiàn)起來步驟還是比較多的,而且里面很多坑也沒有給出。我們隨便解決幾個(gè):
階段四:分布式鎖優(yōu)化
(1)第一個(gè)坑:秒殺商品出現(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)成功秒殺商品,此時(shí)還剩余:" + realGoods + "件");
- }else{
- System.out.println("商品已經(jīng)售罄,歡迎下次活動(dòng)");
- }
- }finally {
- stringRedisTemplate.delete("lock");
- }
- return "success";
- }
此時(shí)我們加一個(gè)try和finally語句就可以了。最終一定要?jiǎng)h除lock。
(2)第二個(gè)坑:秒殺商品時(shí)間太久,其他用戶等不及
- 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)成功秒殺商品,此時(shí)還剩余:" + realGoods + "件");
- }else{
- System.out.println("商品已經(jīng)售罄,歡迎下次活動(dòng)");
- }
- }finally {
- stringRedisTemplate.delete("lock");
- }
- return "success";
- }
給其添加一個(gè)過期時(shí)間,也就是說如果10毫秒內(nèi)沒有秒殺成功,就表示秒殺失敗,換下一個(gè)用戶。
(3)第三個(gè)坑:高并發(fā)場(chǎng)景下,秒殺時(shí)間太久,鎖永久失效問題
我們剛剛設(shè)置的鎖過期時(shí)間是10毫秒,如果一個(gè)用戶秒殺時(shí)間是15毫秒,這也就意味著他可能還沒秒殺成功,就有其他用戶進(jìn)來了。當(dāng)這種情況過多時(shí),就可能有大量用戶還沒秒殺成功其他大量用戶就進(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)成功秒殺商品,此時(shí)還剩余:" + realGoods + "件");
- }else{
- System.out.println("商品已經(jīng)售罄,歡迎下次活動(dòng)");
- }
- }finally {
- if(user.equals(stringRedisTemplate.opsForValue().get("lock"))){
- stringRedisTemplate.delete("lock");
- }
- }
- return "success";
- }
也就是說,我們?cè)趧h除lock的時(shí)候判斷是不是當(dāng)前的線程,如果是那就刪除,如果不是那就不刪除,這樣就算別的線程進(jìn)來也不會(huì)亂刪lock,造成混亂。
OK,到目前為止基本上把分布式鎖的緣由介紹了一遍。對(duì)于分布式鎖redisson完成的相當(dāng)出色,下篇文章也將圍著繞Redisson來介紹一下分布式如何實(shí)現(xiàn),以及其中的原理。
本文轉(zhuǎn)載自微信公眾號(hào)「愚公要移山」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系愚公要移山公眾號(hào)。