分佈式鎖
- 當在分佈式模型下,數據只有一份(或有限制),此時需要利用鎖的技術控制某一時刻修改數據的進程數。
-
與單機模式下的鎖不僅需要保證進程可見,還需要考慮進程與鎖之間的網絡問題。(我覺得分佈式情況下之所以問題變得複雜,主要就是需要考慮到網絡的延時和不可靠。。。一個大坑)
-
分佈式鎖還是可以將標記存在內存,只是該內存不是某個進程分配的內存而是公共內存如Redis、Memcache。至於利用數據庫、文件等做鎖與單機的實現是一樣的,只要保證標記能互斥就行。
實現方式
- 基於數據庫實現
- 基於緩存實現(redis)
- zookeeper實現
本篇基於redis實現分佈式鎖
實現的三個關鍵點
- 原子性:加鎖和解鎖原子操作不可打斷
- 避免死鎖:鎖不會被某個進程一直佔有或者在佔有鎖進程無法解鎖(宕機)情況下能夠解鎖
- 互斥性:同一時刻只能有一個進程獲得鎖
- 正確解鎖:鎖的佔有者只能解除自己的鎖
代碼實現(jedis)
redis配置
host=127.0.0.1
port=6379
password=123456
redis配置類
package com.zyl.redis.config;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
@Configuration
@PropertySource(value = {"classpath:redis-conf.properties"})
public class RedisConfig
{
@Value("${host}")
private String host;
@Value("${port}")
private int port;
@Value("${password}")
private String password;
@Bean
public JedisPool getJedisPool()
{
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxIdle(50);
config.setMaxTotal(120);
config.setMinIdle(20);
config.setMaxWaitMillis(10000);
return new JedisPool(config, host, port, 10000, password);
}
@Bean
public Jedis getJedis()
{
return getJedisPool().getResource();
}
}
鎖
package com.zyl.redis.lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.ArrayList;
import java.util.List;
@Component
public class Lock
{
@Autowired
private JedisPool jedisPool;
//redis2.6支持lua腳本,實現操作原子性
//鎖的內容和當前鎖的內容是否一致,否則不能解鎖表明已經被其它進程獲得
private static final String unlockScript = "if (redis.call('exists',KEYS[1]) == 0 " +
"or redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1]) else return -1 end";
public void tryLock(String key, String value, int exSeconds)
{
//嘗試加鎖;加鎖失敗繼續嘗試
while (!lock(key, value, exSeconds))
{
System.out.println(Thread.currentThread().getName() + " 正在獲取鎖");
}
}
private boolean lock(String key, String value, int exSeconds)
{
//枷鎖
SetParams setParams = new SetParams();
//if not exists;key存在不做任何操作,否則設置
setParams.nx();
//設置過期時間,避免死鎖
setParams.ex(exSeconds);
Jedis jedis = jedisPool.getResource();
String ret = jedis.set(key, value, setParams);
//歸還鏈接,否則連接池鏈接一直被佔用,其它線程不能獲得鏈接
if (null != jedis) jedis.close();
if (!StringUtils.isEmpty(ret) && "OK".equals(ret))
{
System.out.println(Thread.currentThread().getName() + " 加鎖成功");
return true;
}
System.out.println(Thread.currentThread().getName() + " 加鎖失敗");
return false;
}
public void unLock(String key, String value)
{
List<String> keys = new ArrayList<>();
List<String> values = new ArrayList<>();
keys.add(key);
values.add(value);
Jedis jedis = jedisPool.getResource();
//解鎖;執行lua腳本實現原子操作
Object ret = jedis.eval(unlockScript, keys, values);
if (null != jedis) jedis.close();
if (-1 == ((Long)ret))
{
System.out.println(Thread.currentThread().getName() + " 解鎖失敗");
}
else
{
System.out.println(Thread.currentThread().getName() + " 解鎖成功:" + ret);
}
}
}
測試代碼
package com.zyl.redis.service;
import com.zyl.redis.lock.Lock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
public class RedisLockTest
{
private static final String LOCK_KEY = "TEST_LOCK";
private static final int LOCK_EXPIRY_SECONDS = 100;
//線程變量,容易實現了線程數據同步
private ThreadLocal<String> local = new ThreadLocal<>();
public static int count = 0;
@Autowired
private Lock lock;
public void test()
{
Runnable runnable = new Runnable() {
@Override
public void run() {
//爲鎖設置一個隨機變量與線程關聯起來;避免其它線程解鎖
//例如:進程A獲得鎖之後由於業務複雜在過期時間內未完成沒有解鎖
//此時鎖過期進程B獲得鎖;
//進程A業務執行完畢去釋放鎖,此時通過這個隨機值判斷時候自己的鎖
//不是就不會釋放鎖
String uuid = UUID.randomUUID().toString();
local.set(uuid);
lock.tryLock(LOCK_KEY, local.get(), LOCK_EXPIRY_SECONDS);
System.out.println(Thread.currentThread().getName() + " 運行: " + count++);
lock.unLock(LOCK_KEY, local.get());
}
};
Thread thread = null;
for (int i = 0; i < 50; i++)
{
thread = new Thread(runnable, "Thread-" + i);
thread.start();
}
}
}
測試截圖
maven
<?xml version="1.0" encoding="UTF-8"?>
<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.zyl</groupId>
<artifactId>redislock</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.0.1</version>
</dependency>
</dependencies>
</project>