什么是分布式鎖
鎖是什么我們當(dāng)然知道肌幽,在多線程程序中,不予許多個線程同時操作某個變量或者同時執(zhí)行某一代碼塊抓半,我們就需要用鎖來實(shí)現(xiàn)喂急。在Java中,可以用synchronized或Lock接口的實(shí)現(xiàn)類來實(shí)現(xiàn)琅关。那么什么是分布式鎖呢煮岁?當(dāng)我們的應(yīng)用通過分布式部署,每個應(yīng)用部署在不同的機(jī)器上涣易,但是我們要保證這些不同機(jī)器上的同一方法在同一時間不能被多個線程執(zhí)行画机,這時候就要用到分布式鎖。分布式鎖有很多種實(shí)現(xiàn)方式新症,這里我們介紹Redis實(shí)現(xiàn)方式步氏。
基于 redis的 SETNX()、EXPIRE() 方法做分布式鎖
-SETNX()
setnx接收兩個參數(shù)key徒爹,value荚醒。如果key存在,則不做任何操作隆嗅,返回0界阁,若key不存在,則設(shè)置成功胖喳,返回1泡躯。
-EXPIRE()
expire 設(shè)置過期時間,要注意的是 setnx 命令不能設(shè)置 key 的超時時間,只能通過 expire() 來對 key 設(shè)置较剃。
首先去redis官網(wǎng)下載redis咕别,將文件解壓,運(yùn)行redis-server.exe啟動redis服務(wù)写穴。新建一個SpringBoot項(xiàng)目惰拱,添加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 清風(fēng)
* @email 1737543007@qq.com
* @date 19-3-5
*/
@Configuration
public class RedisConfig {
/**
* 注入 RedisConnectionFactory
*/
@Autowired
RedisConnectionFactory redisConnectionFactory;
/**
* 實(shí)例化 RedisTemplate 對象
*
* @return
*/
@Bean
public RedisTemplate<String, Object> functionDomainRedisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* 設(shè)置數(shù)據(jù)存入 redis 的序列化方式
*
* @param redisTemplate
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
redisTemplate.setConnectionFactory(factory);
}
/**
* 實(shí)例化 HashOperations 對象,可以使用 Hash 類型操作
*
* @param redisTemplate
* @return
*/
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
/**
* 實(shí)例化 ValueOperations 對象,可以使用 String 操作
*
* @param redisTemplate
* @return
*/
@Bean
public ValueOperations<String, Object> valueOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForValue();
}
/**
* 實(shí)例化 ListOperations 對象,可以使用 List 操作
*
* @param redisTemplate
* @return
*/
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
/**
* 實(shí)例化 SetOperations 對象,可以使用 Set 操作
*
* @param redisTemplate
* @return
*/
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
/**
* 實(shí)例化 ZSetOperations 對象,可以使用 ZSet 操作
*
* @param redisTemplate
* @return
*/
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
在實(shí)現(xiàn)redis分布式鎖之前,我們先分析一下需要注意的問題:
1.加鎖過程必須設(shè)置過期時間啊送,加鎖和設(shè)置過期時間過程必須是原子操作
如果沒有設(shè)置過期時間偿短,那么就發(fā)生死鎖,鎖永遠(yuǎn)不能被釋放删掀。如果加鎖后服務(wù)宕機(jī)或程序崩潰翔冀,來不及設(shè)置過期時間,同樣會發(fā)生死鎖披泪。
2.解鎖必須是解除自己加上的鎖
試想一個這樣的場景纤子,服務(wù)A加鎖,但執(zhí)行效率非常慢款票,導(dǎo)致鎖失效后還未執(zhí)行完控硼,但這時候服務(wù)B已經(jīng)拿到鎖了,這時候服務(wù)A執(zhí)行完畢了去解鎖艾少,把服務(wù)B的鎖給解掉了卡乾,其他服務(wù)C、D缚够、E...都可以拿到鎖了幔妨,這就有問題了。加鎖的時候我們可以設(shè)置唯一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 清風(fēng)
* @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.SetOption.SET_IF_ABSENT);
if(result!=null && result)
return token;
}finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
return null;
}
/**
* 解鎖
* @param name
* @param token
* @return
*/
public boolean unlock(String name, String token) {
byte[][] keysAndArgs = new byte[2][];
keysAndArgs[0] = name.getBytes(Charset.forName("UTF-8"));
keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
try {
Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
if(result!=null && result>0)
return true;
}finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
return false;
}
}
使用
String token = null;
try{
token = redisLock.lock("lock_name", 10000, 11000);
if(token != null) {
System.out.print("我拿到了鎖哦")
// 執(zhí)行業(yè)務(wù)代碼
} else {
System.out.print("我沒有拿到鎖唉")
}
} finally {
if(token!=null) {
redisLock.unlock("lock_name", token);
}
}
That's all