Java分散式鎖方案和區別 - Redis,Zookeeper,資料庫

oktokeep發表於2024-08-09

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 > 快取 > 資料庫

相關文章