轉自:
https://www.jianshu.com/p/750ac97eb29e
什么是分布式鎖
鎖是什么我們當然知道,在多線程程序中,不予許多個線程同時操作某個變量或者同時執行某一代碼塊,我們就需要用鎖來實現。在Java中,可以用synchronized或Lock接口的實現類來實現。那么什么是分布式鎖呢?當我們的應用通過分布式部署,每個應用部署在不同的機器上,但是我們要保證這些不同機器上的同一方法在同一時間不能被多個線程執行,這時候就要用到分布式鎖。分布式鎖有很多種實現方式,這里我們介紹Redis實現方式。
基於 redis的 SETNX()、EXPIRE() 方法做分布式鎖
-SETNX()
setnx接收兩個參數key,value。如果key存在,則不做任何操作,返回0,若key不存在,則設置成功,返回1。
-EXPIRE()
expire 設置過期時間,要注意的是 setnx 命令不能設置 key 的超時時間,只能通過 expire() 來對 key 設置。
首先去redis官網下載redis,將文件解壓,運行redis-server.exe啟動redis服務。新建一個SpringBoot項目,添加redis依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
在application.yml配置文件加上redis配置
spring: redis: host: 127.0.0.1 port: 6379
新建redis配置類RedisConfig.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.*; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; /** * @author 清風 * @email 1737543007@qq.com * @date 19-3-5 */
在實現redis分布式鎖之前,我們先分析一下需要注意的問題:
1.加鎖過程必須設置過期時間,加鎖和設置過期時間過程必須是原子操作
如果沒有設置過期時間,那么就發生死鎖,鎖永遠不能被釋放。如果加鎖后服務宕機或程序崩潰,來不及設置過期時間,同樣會發生死鎖。
2.解鎖必須是解除自己加上的鎖
試想一個這樣的場景,服務A加鎖,但執行效率非常慢,導致鎖失效后還未執行完,但這時候服務B已經拿到鎖了,這時候服務A執行完畢了去解鎖,把服務B的鎖給解掉了,其他服務C、D、E...都可以拿到鎖了,這就有問題了。加鎖的時候我們可以設置唯一value,解鎖時判斷是不是自己先前的value就行了。
redis鎖代碼
import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.core.RedisConnectionUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.stereotype.Repository; import java.nio.charset.Charset; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * @author 清風 * @email 1737543007@qq.com * @date 19-3-5 */ @Repository public class RedisLock { /** * 解鎖腳本,原子操作 */ private static final String unlockScript = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" + "then\n" + " return redis.call(\"del\",KEYS[1])\n" + "else\n" + " return 0\n" + "end"; private StringRedisTemplate redisTemplate; public RedisLock(StringRedisTemplate redisTemplate) { this.redisTemplate = redisTemplate; } /** * 加鎖,有阻塞 * @param name * @param expire * @param timeout * @return */ public String lock(String name, long expire, long timeout){ long startTime = System.currentTimeMillis(); String token; do{ token = tryLock(name, expire); if(token == null) { if((System.currentTimeMillis()-startTime) > (timeout-50)) break; try { Thread.sleep(50); //try 50 per sec } catch (InterruptedException e) { e.printStackTrace(); return null; } } }while(token==null); return token; } /** * 加鎖,無阻塞 * @param name * @param expire * @return */ public String tryLock(String name, long expire) { String token = UUID.randomUUID().toString(); RedisConnectionFactory factory = redisTemplate.getConnectionFactory(); RedisConnection conn = factory.getConnection(); try{ Boolean result = conn.set(name.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")), Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands