分布式鎖的實現(xiàn)
在常見的分布式鎖中有以下三種實現(xiàn):
- Redis 實現(xiàn)
- Zookeeper 實現(xiàn)
- 數(shù)據(jù)庫實現(xiàn)
1. 基于 Redis 的實現(xiàn)
在 Redis 中有 3 個重要命令芋膘,通過這三個命令可以實現(xiàn)分布式鎖
- setnx key val:當且僅當key不存在時表牢,set一個key為val的字符串嗅回,返回1之众;若key存在,則什么都不做蝇率,返回0迟杂。
- expire key timeout:為key設置一個超時時間刽沾,單位為second,超過這個時間 key 會自動刪除排拷。
- delete key:刪除key
1.1 實現(xiàn)原理
- 獲取鎖的時候侧漓,使用 setnx 命令設置一個 kv,其中 k 為鎖的名字监氢,v 為一個隨機數(shù)字布蔗,如果成功設置則獲取鎖,如果未設置成功則失敗浪腐。如果設置了嘗試獲取鎖的最大的時間纵揍,則需要在最大時間內,不停的重復該步驟议街,直到獲取鎖或者超過最大時間才能結束泽谨。
- 使用 expire 命令為剛才創(chuàng)建的 key 設置超時一個合理的超時時間,防止在無法正確釋放鎖的時候也能通過超時時間進行釋放特漩,這個超時時間需要根據(jù)項目請求情況進行設置吧雹;
- 釋放鎖的時候,通過 v 判斷是不是還是原來的鎖涂身,若是該鎖雄卷,則執(zhí)行 delete 進行鎖釋放。
1.2 實現(xiàn)方式
1.2.1 原生代碼
public class DistributedLock implements Lock {
private static JedisPool JEDIS_POOL = null;
private static int EXPIRE_SECONDS = 60;
public static void setJedisPool(JedisPool jedisPool, int expireSecond) {
JEDIS_POOL = jedisPool;
EXPIRE_SECONDS = expireSecond;
}
private String lockKey;
private String lockValue;
private DistributedLock(String lockKey) {
this.lockKey = lockKey;
}
public static DistributedLock newLock(String lockKey) {
return new DistributedLock(lockKey);
}
@Override
public void lock() {
if (!tryLock()) {
throw new IllegalStateException("未獲取到鎖");
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return tryLock(0, null);
}
@Override
public boolean tryLock(long time, TimeUnit unit) {
Jedis conn = null;
String retIdentifier = null;
try {
conn = JEDIS_POOL.getResource();
lockKey = UUID.randomUUID().toString();
// 獲取鎖的超時時間蛤售,超過這個時間則放棄獲取鎖
long end = 0;
if (time != 0) {
end = System.currentTimeMillis() + unit.toMillis(time);
}
do {
if (conn.setnx(lockKey, lockValue) == 1) {
conn.expire(lockKey, EXPIRE_SECONDS);
return true;
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
} while (System.currentTimeMillis() < end);
} catch (JedisException e) {
if (lockValue.equals(conn.get(lockKey))) {
conn.del(lockKey);
}
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return false;
}
@Override
public void unlock() {
Jedis conn = null;
try {
conn = JEDIS_POOL.getResource();
if (lockValue.equals(conn.get(lockKey))) {
conn.del(lockKey);
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
}
@Override
public Condition newCondition() {
return null;
}
}
上面的代碼中也有一個問題丁鹉,setnx 和 expire 是分為兩步進行了,雖然在 catch 中處理異常并嘗試將可能出現(xiàn)鎖刪除悴能,但這種方式并不友好揣钦,一個好的方案是通過執(zhí)行 lua 腳本來實現(xiàn)。在 Spring Redis Lock 和 Redission 都是通過 lua 腳本實現(xiàn)的
local lockClientId = redis.call('GET', KEYS[1])
if lockClientId == ARGV[1] then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return true
elseif not lockClientId then
redis.call('SET', KEYS[1], ARGV[1], 'PX', ARGV[2])
return true
end
return false
1.2.2 Spring Redis Lock 實現(xiàn)
1. 引入庫
在 Spring Boot 項目會根據(jù) Spring Boot 依賴管理自動配置版本號
Maven
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置 redis
在 application-xxx.yml
中配置
spring:
redis:
host: 127.0.0.1
port: 6379
timeout: 2500
password: xxxxx
3. 增加配置
RedisLockConfig.java
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.integration.redis.util.RedisLockRegistry;
@Configuration
public class RedisLockConfig {
@Bean
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory redisConnectionFactory) {
return new RedisLockRegistry(redisConnectionFactory, "redis-lock",
TimeUnit.MINUTES.toMillis(10));
}
}
4. 使用
@Autowired
private RedisLockRegistry lockRegistry;
Lock lock = lockRegistry.obtain(key);
boolean locked = false;
try {
locked = lock.tryLock();
if (!locked) {
// 沒有獲取到鎖的邏輯
}
// 獲取鎖的邏輯
} finally {
// 一定要解鎖
if (locked) {
lock.unlock();
}
}
1.2.3 Redission 實現(xiàn)
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("xxxxxx").setDatabase(0);
RedissonClient redissonClient = Redisson.create(config);
RLock rLock = redissonClient.getLock("lockKey");
boolean locked = false;
try {
/*
* waitTimeout 嘗試獲取鎖的最大等待時間搜骡,超過這個值,則認為獲取鎖失敗
* leaseTime 鎖的持有時間,超過這個時間鎖會自動失效
*/
locked = rLock.tryLock((long) waitTimeout, (long) leaseTime, TimeUnit.SECONDS);
if (!locked) {
// 沒有獲取鎖的邏輯
}
// 獲取鎖的邏輯
} catch (Exception e) {
throw new RuntimeException("aquire lock fail");
} finally {
if(locked)
rLock.unlock();
}
1.3 優(yōu)缺點
優(yōu)點:redis 本身的性能比較高佑女,即使存在大量的 setnx 命令也不會有所下降
缺點:
- 如果 key 設置的超時時間過短可能導致業(yè)務流程還沒處理完鎖就釋放了记靡,導致其他請求也能獲取到鎖
- 如果 key 設置的超時時間過大,且未釋放鎖团驱,會導致一些請求長時間在等待鎖
- 在鎖不斷嘗試的過程中摸吠,會浪費 CPU 資源
針對第 2 個缺點,在 Redission 通過續(xù)約機制嚎花,每隔一段時間去檢測鎖是否還在進行寸痢,如果還在運行就將對應的 key 增加一定的時間,保證在鎖運行的情況下不會發(fā)生 key 到了過期時間自動刪除的情況
2. 基于 Zookeeper 的實現(xiàn)
2.1 實現(xiàn)原理
基于zookeeper臨時有序節(jié)點可以實現(xiàn)的分布式鎖紊选。
大致步驟:客戶端對某個方法加鎖時啼止,在 zookeeper 上的與該方法對應的指定節(jié)點的目錄下道逗,生成一個唯一的臨時有序節(jié)點。 判斷是否獲取鎖的方式很簡單献烦,只需要判斷有序節(jié)點中序號最小的一個滓窍。 當釋放鎖的時候,只需將這個瞬時節(jié)點刪除即可巩那。同時吏夯,其可以避免服務宕機導致的鎖無法釋放,而產生的死鎖問題即横。
當?shù)谝粋€節(jié)點申請鎖 xxxlock 時如下: 在 xxxlock 持久節(jié)點下噪生,創(chuàng)建一個 lock 的臨時有序節(jié)點,此時因為 lock 為有序節(jié)點中序號最小的一個东囚,則此時獲取到鎖
當?shù)谝粋€節(jié)點還在處理業(yè)務邏輯未釋放鎖時跺嗽,第二節(jié)點申請 xxxlock 鎖,創(chuàng)建一個 lock 的臨時有序節(jié)點舔庶,此時因為 lock 不是有序節(jié)點中序號最小的一個抛蚁,則此時不能獲取到鎖,需要一直等到 lock:1 節(jié)點刪除后才能獲取到鎖惕橙,此時 lock:2 會 watch 它的上一個節(jié)點(即 lock:1)等到 lock:1 刪除后在獲取鎖
當?shù)谝粋€節(jié)點還在處理業(yè)務邏輯未釋放鎖時瞧甩,第二節(jié)點還在排隊,第三個節(jié)點申請鎖時弥鹦,創(chuàng)建一個 lock 的臨時有序節(jié)點肚逸,此時因為 lock 不是有序節(jié)點中序號最小的一個,則此時不能獲取到鎖彬坏,需要一直等到上面的節(jié)點( lock:1 和 lock:2 )節(jié)點刪除后才能獲取到鎖朦促,此時 lock:3 會 watch 它的上一個節(jié)點(即 lock:2)等到 lock:2 刪除后在獲取鎖
2.2 使用
2.2.1 使用 spring-integration-zookeeper 實現(xiàn)
Maven.
<dependency>
<!-- spring integration -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-zookeeper</artifactId>
</dependency>
Gradle.
compile "org.springframework.integration:spring-integration-zookeeper:5.1.2.RELEASE"
增加配置
@Configuration
public class ZookeeperLockConfig {
@Value("${zookeeper.host}")
private String zkUrl;
@Bean
public CuratorFrameworkFactoryBean curatorFrameworkFactoryBean() {
return new CuratorFrameworkFactoryBean(zkUrl);
}
@Bean
public ZookeeperLockRegistry zookeeperLockRegistry(CuratorFramework curatorFramework) {
return new ZookeeperLockRegistry(curatorFramework, "/lock");
}
}
使用
@Autowired
private ZookeeperLockRegistry lockRegistry;
Lock lock = lockRegistry.obtain(key);
boolean locked = false;
try {
locked = lock.tryLock();
if (!locked) {
// 沒有獲取到鎖的邏輯
}
// 獲取鎖的邏輯
} finally {
// 一定要解鎖
if (locked) {
lock.unlock();
}
}
2.2.2 使用 Apache Curator
Maven
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>5.1.0</version>
</dependency>
使用
CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(
connectString,
sessionTimeoutMs,
connectionTimeoutMs,
new RetryNTimes(retryCount, elapsedTimeMs));
InterProcessMutex mutex = new InterProcessMutex(curatorFramework, "lock name");
mutex.acquire(); // 獲取鎖
mutex.acquire(long time, TimeUnit unit) // 獲取鎖并設置最大等待時間
mutex.release(); // 釋放鎖
2.3 優(yōu)缺點
優(yōu)點:
- 解決了單點問題,通過集群部署 zookeeper栓始;
- 因為用的臨時節(jié)點务冕,在項目出現(xiàn)意外的情況下可以保證鎖可以釋放,當 session 異常斷開時幻赚,臨時節(jié)點會自動刪除禀忆;
- 不用在設置存儲過期時間,避免了 Redis 鎖過期引發(fā)的問題落恼;
缺點:
- 性能不如 Redis 實現(xiàn)箩退;
3. 基于數(shù)據(jù)庫的實現(xiàn)
3.1 實現(xiàn)原理
create table distributed_lock (
id int(11) unsigned NOT NULL auto_increment primary key,
key_name varchar(30) unique NOT NULL comment '鎖名',
update_time datetime default current_timestamp on update current_timestamp comment '更新時間'
)ENGINE=InnoDB comment '數(shù)據(jù)庫鎖';
方式一:通過 insert 和 delete 實現(xiàn)
使用數(shù)據(jù)庫唯一索引,當我們想獲取一個鎖的時候佳谦,就 insert 一條數(shù)據(jù)戴涝,如果 insert 成功則獲取到鎖,獲取鎖之后,通過 delete 語句來刪除鎖
這種方式實現(xiàn)啥刻,鎖不會等待奸鸯,如果想設置獲取鎖的最大時間,需要自己實現(xiàn)
方式二:通過for update 實現(xiàn)
以下操作需要在事務中進行
select * from distributed_lock where key_name = 'lock' for update;
在查詢語句后面增加 for update
郑什,數(shù)據(jù)庫會在查詢過程中給數(shù)據(jù)庫表增加排他鎖府喳。當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖蘑拯。for update
的另一個特性就是會阻塞钝满,這樣也間接實現(xiàn)了一個阻塞隊列,但是 for update
的阻塞時間是由數(shù)據(jù)庫決定的申窘,而不是程序決定的弯蚜。
在 MySQL 8 中,for update
語句可以加上 nowait
來實現(xiàn)非阻塞用法
select * from distributed_lock where key_name = 'lock' for update nowait;
在 InnoDB 引擎在加鎖的時候剃法,只有通過索引查詢時才會使用行級鎖碎捺,否則為表鎖,而且如果查詢不到數(shù)據(jù)的時候也會升級為表鎖贷洲。
這種方式需要在數(shù)據(jù)庫中實現(xiàn)已經存在數(shù)據(jù)的情況下使用收厨。
3.2 優(yōu)缺點
優(yōu)點:
如果項目中已經使用了數(shù)據(jù)庫在不引入其他中間件的情況下,可以直接使用數(shù)據(jù)庫优构,減少依賴
直接借助數(shù)據(jù)庫诵叁,容易理解。
缺點:
- 操作數(shù)據(jù)庫需要一定的開銷钦椭,性能問題需要考慮拧额;
- 使用數(shù)據(jù)庫的行級鎖并不一定靠譜,尤其是當我們的鎖表并不大的時候彪腔;
- 沒有鎖超時機制侥锦,導致必須自己刪除,故障后如何刪除鎖成為一個問題
- for update 方式必須在事務內部德挣,如果業(yè)務操作不能在事務里面執(zhí)行又是一個問題
- 各種各樣的問題恭垦,在解決問題的過程中會使整個方案變得越來越復雜。
4. 對比
從性能角度(從高到低)緩存 > Zookeeper >= 數(shù)據(jù)庫
從可靠性角度(從高到低)Zookeeper > 緩存 > 數(shù)據(jù)庫
問題格嗅、實現(xiàn) | Redis | Zookeeper | 數(shù)據(jù)庫 |
---|---|---|---|
性能 | 高 | 中 | 低 |
可靠性 | 中 | 高 | 低 |
過期刪除 | 有番挺,設置過期時間,或者手動刪除 | 執(zhí)行業(yè)務邏輯后手動刪除 | 1. for update 事務完成后吗浩,數(shù)據(jù)庫自動釋放 2. insert 方式執(zhí)行業(yè)務邏輯后手動刪除 |
阻塞隊列 | 無建芙,需要客戶端自旋解決 | 通過監(jiān)聽上一個 lock 解決没隘,watch 機制 | 1. for update 數(shù)據(jù)庫自己解決 2. insert 方式需要客戶端自旋解決 |
超時時間內業(yè)務未完成問題 | 需要自己寫續(xù)約機制完成懂扼,Redission 內部自己實現(xiàn)了 | 無這問題 | 1. for update 執(zhí)行時間過長,可能導致事務本身超時 2. insert 方式無此問題 |
項目異常導致鎖未手動刪除的情況 | redis 有過期時間,過期時間后自動刪除 | session 斷開后阀湿,臨時節(jié)點自動刪除 | 1. for update 機制數(shù)據(jù)庫會自動清除 2. insert 方式就得自己想解決方案了 |