Java分散式鎖方案和區別 - Redis,Zookeeper,資料庫
1. 基於 Redis 的實現
在 Redis 中有 3 個重要命令,透過這三個命令可以實現分散式鎖
setnx key val:當且僅當key不存在時,set一個key為val的字串,返回1;若key存在,則什麼都不做,返回0。
expire key timeout:為key設定一個超時時間,單位為second,超過這個時間 key 會自動刪除。
delete key:刪除key
Redission 實現
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
import org.redisson.Redisson; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import java.util.concurrent.TimeUnit; public class RedissonTest { /** * 未獲取到鎖 * 未釋放鎖 * @param args */ public static void main(String[] args) { 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("lockKey240808"); boolean locked = false; try { /* * waitTimeout 嘗試獲取鎖的最大等待時間,超過這個值,則認為獲取鎖失敗 * leaseTime 鎖的持有時間,超過這個時間鎖會自動失效 */ locked = rLock.tryLock((long) 30, (long) 300, TimeUnit.SECONDS); if (!locked) { // 沒有獲取鎖的邏輯 System.out.println("未獲取到鎖"); }else{ // 獲取鎖的邏輯 System.out.println("獲取到鎖"); } } catch (Exception e) { throw new RuntimeException("aquire lock fail"); } finally { // if(locked) { // rLock.unlock(); // System.out.println("釋放鎖"); // } System.out.println("未釋放鎖"); } } }
檢視redisson key資料型別
type lockKey240808 hash hgetall lockKey240808 1) 0d46aa7f-424f-45f3-b3a8-b56ec4f59ce6:1 2) 1
redis hset 雜湊表操作新增json串為單引號且客戶端視窗需要最大化,字串不能斷行
https://www.cnblogs.com/oktokeep/p/16999417.html
注意點:
waitTime 為了獲取鎖願意等待的時長 <= 0 不願意等待,即沒有獲取到鎖時直接返回false
leaseTime 加鎖成功後自動釋放鎖的時長:
>0 時 不論鎖定的業務是否執行完畢都會在這個時間到期時釋放鎖---這個很要命(一定不會死鎖);肯能會存線上程1執行業務沒有完畢,鎖自動釋放了,執行緒2獲取到鎖執行了業務,鎖失效了;
=-1表示這個鎖不會自動釋放必須手動釋放(可能會死鎖),看門狗每10秒(預設配置)延期一次鎖(實際是重置鎖的過期時間為30秒:預設配置)
Redission 透過續約機制,每隔一段時間去檢測鎖是否還在進行,如果還在執行就將對應的 key 增加一定的時間,保證在鎖執行的情況下不會發生 key 到了過期時間自動刪除的情況
2. 基於 Zookeeper 的實現
2.1 實現原理
基於zookeeper臨時有序節點可以實現的分散式鎖。
大致步驟:客戶端對某個方法加鎖時,在 zookeeper 上的與該方法對應的指定節點的目錄下,生成一個唯一的臨時有序節點。 判斷是否獲取鎖的方式很簡單,只需要判斷有序節點中序號最小的一個。 當釋放鎖的時候,只需將這個瞬時節點刪除即可。同時,其可以避免服務當機導致的鎖無法釋放,而產生的死鎖問題。
使用:
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(); } }
3. 基於資料庫的實現
3.1 實現原理
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 '資料庫鎖';
方式一:透過 insert 和 delete 實現
使用資料庫唯一索引,當我們想獲取一個鎖的時候,就 insert 一條資料,如果 insert 成功則獲取到鎖,獲取鎖之後,透過 delete 語句來刪除鎖
這種方式實現,鎖不會等待,如果想設定獲取鎖的最大時間,需要自己實現
方式二:透過for update 實現
以下操作需要在事務中進行
select * from distributed_lock where key_name = 'lock' for update;
在查詢語句後面增加 for update,資料庫會在查詢過程中給資料庫表增加排他鎖。當某條記錄被加上排他鎖之後,其他執行緒無法再在該行記錄上增加排他鎖。for update 的另一個特性就是會阻塞,這樣也間接實現了一個阻塞佇列,但是 for update 的阻塞時間是由資料庫決定的,而不是程式決定的。
在 MySQL 8 中,for update 語句可以加上 nowait 來實現非阻塞用法
select * from distributed_lock where key_name = 'lock' for update nowait;
在 InnoDB 引擎在加鎖的時候,只有透過索引查詢時才會使用行級鎖,否則為表鎖,而且如果查詢不到資料的時候也會升級為表鎖。
這種方式需要在資料庫中實現已經存在資料的情況下使用。
對比
從效能角度(從高到低)快取 > Zookeeper >= 資料庫
從可靠性角度(從高到低)Zookeeper > 快取 > 資料庫