摘要
當在分佈式模型下,數據只有一份,此時需要利用鎖的技術控制某一時刻修改數據的進程數。與單機模式下的鎖不同,分佈式鎖不僅僅需要保證不同進程訪問對象有鎖,還需要保證不同主機的不同系統訪問該對象時有鎖。所以通常我們會爲需要枷鎖的對象添加狀態,分佈式系統中鎖的狀態通常存儲在外部公共存儲中,例如redis、zookeeper、文件系統甚至數據庫中。
本文將基於redis實現分佈式鎖。
加鎖
加鎖其實就是向redis添加key-value。爲了避免死鎖,需要設置過期時間。
- NX 代表只在鍵不存在時,纔對鍵進行設置操作
SET lock_key lock_value NX PX 5000
如果上面的命令執行成功,則證明客戶端獲取到了鎖。
解鎖
解鎖其實就是刪除redis中添加的key。但也不能亂刪,不能客戶端1的請求將客戶端2的鎖給刪除掉。需要根據lock_value過濾。需要注意的是爲了保證redis操作的原子性,需要通過lua命令刪除。
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then " );
sb.append(" return redis.call('expire',KEYS[1],ARGV[2]) ");
sb.append(" else ");
sb.append("return 0 ");
sb.append(" end ");
lua_expire=sb.toString();
}
jedis.eval(lua_del, 1, lockKey, lockValue);
過期時間
爲了避免死鎖設置了過期時間,同時又要保證過期時間不能低於代碼執行時間。所以單獨添加一個線程刷新過期時間。同時爲了保證redis操作原子性,通過lua腳本執行。
/**
* 開啓定時刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
}
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
try{
System.out.println("[key="+lockKey+"]延遲失效時間");
jedis.eval(lua_expire,1, lockKey, lockValue, String.valueOf(EXPIRE_TIME));
//休眠10秒
sleepBySecond(EXPIRE_TIME-1);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
使用方式
RedisLock lock = new RedisLock("testRedisLock");
lock.lock();
//模擬業務執行15秒
System.out.println("執行方法:"+id);
lock.sleepBySecond(15);
lock.unlock();
代碼
public class RedisLock implements Lock {
private static final long EXPIRE_TIME=100;
private static final String NOT_EXIST="NX";
private static final String SECOND="EX";
private static final String OK="OK";
protected volatile boolean isOpenExpirationRenewal = true;
private Jedis jedis=null;
private String lockKey="";
private String lockValue="";
private static String lua_del= "";
private static String lua_expire= "";
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then ");
sb.append(" return redis.call('del', KEYS[1]) ");
sb.append(" else ");
sb.append("return 0 ");
sb.append(" end ");
lua_del=sb.toString();
}
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call('get', KEYS[1]) == ARGV[1] then " );
sb.append(" return redis.call('expire',KEYS[1],ARGV[2]) ");
sb.append(" else ");
sb.append("return 0 ");
sb.append(" end ");
lua_expire=sb.toString();
}
/**
* 獲取redis連接,隨機生成value
* @param lockKey
*/
public RedisLock(String lockKey){
this.lockKey=lockKey;
this.lockValue= UUID.randomUUID().toString()+Thread.currentThread().getId();
JedisPool pool=SpringContextUtil.getBean(JedisPool.class);
this.jedis= pool.getResource();
}
/**
* 加鎖
*/
@Override
public void lock() {
while(true){
String result = jedis.set(lockKey, lockValue, NOT_EXIST,SECOND,EXPIRE_TIME);
if(OK.equals(result)){
System.out.println("[key="+lockKey+"]已加鎖");
//添加單獨線程刷新過期時間
isOpenExpirationRenewal = true;
scheduleExpirationRenewal();
break;
}
}
}
/**
* 解鎖
*/
@Override
public void unlock() {
jedis.eval(lua_del, 1, lockKey, lockValue);
System.out.println("[key="+lockKey+"]已解鎖");
isOpenExpirationRenewal = false;
}
@Override
public void lockInterruptibly(){}
@Override
public Condition newCondition() {
return null;
}
@Override
public boolean tryLock() {
return false;
}
@Override
public boolean tryLock(long time, TimeUnit unit){
return false;
}
/**
* 線程休眠
* @param second
*/
public void sleepBySecond(long second){
try {
Thread.sleep(second*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 開啓定時刷新
*/
protected void scheduleExpirationRenewal(){
Thread renewalThread = new Thread(new ExpirationRenewal());
renewalThread.start();
}
private class ExpirationRenewal implements Runnable{
@Override
public void run() {
while (isOpenExpirationRenewal){
try{
System.out.println("[key="+lockKey+"]延遲失效時間");
jedis.eval(lua_expire,1, lockKey, lockValue, String.valueOf(EXPIRE_TIME));
//休眠10秒
sleepBySecond(EXPIRE_TIME-1);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
}
}