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