選用Redis實(shí)現(xiàn)分布式鎖原因:
-Redis有很高的性能
-Redis命令對(duì)此支持較好,實(shí)現(xiàn)起來比較方便
使用命令介紹:
1.SETNX
SETNX key val
當(dāng)且僅當(dāng)key不存在時(shí),set一個(gè)key為val的字符串餐抢,返回1输莺;若key存在靡狞,則什么都不做蔬咬,返回0。
2.expire
expire key timeout
為key設(shè)置一個(gè)超時(shí)時(shí)間斋日,單位為second,超過這個(gè)時(shí)間鎖會(huì)自動(dòng)釋放墓陈,避免死鎖恶守。
3.delete
delete key
刪除key
實(shí)現(xiàn)思想:
1.獲取鎖的時(shí)候,使用setnx加鎖贡必,并使用expire命令為鎖添加一個(gè)超時(shí)時(shí)間兔港,超過該時(shí)間則自動(dòng)釋放鎖,鎖的value值為一個(gè)隨機(jī)生成的UUID仔拟,通過此在釋放鎖的時(shí)候進(jìn)行判斷衫樊。
2.獲取鎖的時(shí)候還設(shè)置一個(gè)獲取的超時(shí)時(shí)間,若超過這個(gè)時(shí)間則放棄獲取鎖利花。
3.釋放鎖的時(shí)候科侈,通過UUID判斷是不是該鎖,若是該鎖炒事,則執(zhí)行delete進(jìn)行鎖釋放兑徘。
分布式鎖的核心代碼如下:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;
import java.util.List;
import java.util.UUID;
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加鎖
* @param locaName 鎖的key
* @param acquireTimeout 獲取超時(shí)時(shí)間
* @param timeout 鎖的超時(shí)時(shí)間
* @return 鎖標(biāo)識(shí)
*/
public String lockWithTimeout(String locaName,
long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 獲取連接
conn = jedisPool.getResource();
// 隨機(jī)生成一個(gè)value
String identifier = UUID.randomUUID().toString();
// 鎖名,即key值
String lockKey = "lock:" + locaName;
// 超時(shí)時(shí)間羡洛,上鎖后超過此時(shí)間則自動(dòng)釋放鎖
int lockExpire = (int)(timeout / 1000);
// 獲取鎖的超時(shí)時(shí)間挂脑,超過這個(gè)時(shí)間則放棄獲取鎖
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值,用于釋放鎖時(shí)間確認(rèn)
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key沒有設(shè)置超時(shí)時(shí)間欲侮,為key設(shè)置一個(gè)超時(shí)時(shí)間
if (conn.ttl(lockKey) == -1) {
conn.expire(lockKey, lockExpire);
}
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retIdentifier;
}
/**
* 釋放鎖
* @param lockName 鎖的key
* @param identifier 釋放鎖的標(biāo)識(shí)
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 監(jiān)視lock崭闲,準(zhǔn)備開始事務(wù)
conn.watch(lockKey);
// 通過前面返回的value值判斷是不是該鎖,若是該鎖威蕉,則刪除刁俭,釋放鎖
if (identifier.equals(conn.get(lockKey))) {
Transaction transaction = conn.multi();
transaction.del(lockKey);
List<Object> results = transaction.exec();
if (results == null) {
continue;
}
retFlag = true;
}
conn.unwatch();
break;
}
} catch (JedisException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.close();
}
}
return retFlag;
}
}
測試
下面就用一個(gè)簡單的例子測試剛才實(shí)現(xiàn)的分布式鎖。
例子中使用50個(gè)線程模擬秒殺一個(gè)商品韧涨,使用--運(yùn)算符來實(shí)現(xiàn)商品減少牍戚,從結(jié)果有序性就可以看出是否為加鎖狀態(tài)侮繁。
模擬秒殺服務(wù),在其中配置了jedis線程池如孝,在初始化的時(shí)候傳給分布式鎖宪哩,供其使用。
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class Service {
private static JedisPool pool = null;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 設(shè)置最大連接數(shù)
config.setMaxTotal(200);
// 設(shè)置最大空閑數(shù)
config.setMaxIdle(8);
// 設(shè)置最大等待時(shí)間
config.setMaxWaitMillis(1000 * 100);
// 在borrow一個(gè)jedis實(shí)例時(shí)第晰,是否需要驗(yàn)證锁孟,若為true,則所有jedis實(shí)例均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
DistributedLock lock = new DistributedLock(pool);
int n = 500;
public void seckill() {
// 返回鎖的value值茁瘦,供釋放鎖時(shí)候進(jìn)行判斷
String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
System.out.println(Thread.currentThread().getName() + "獲得了鎖");
System.out.println(--n);
lock.releaseLock("resource", indentifier);
}
}
模擬線程進(jìn)行秒殺服務(wù)
public class ThreadA extends Thread {
private Service service;
public ThreadA(Service service) {
this.service = service;
}
@Override
public void run() {
service.seckill();
}
}
public class Test {
public static void main(String[] args) {
Service service = new Service();
for (int i = 0; i < 50; i++) {
ThreadA threadA = new ThreadA(service);
threadA.start();
}
}
}
結(jié)果如下品抽,結(jié)果為有序的。
若注釋掉使用鎖的部分:
public void seckill() {
// 返回鎖的value值甜熔,供釋放鎖時(shí)候進(jìn)行判斷
//String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
System.out.println(Thread.currentThread().getName() + "獲得了鎖");
System.out.println(--n);
//lock.releaseLock("resource", indentifier);
}
從結(jié)果可以看出圆恤,有一些是異步進(jìn)行的。
總結(jié)
可以使用緩存來代替數(shù)據(jù)庫來實(shí)現(xiàn)分布式鎖腔稀,這個(gè)可以提供更好的性能哑了,同時(shí),很多緩存服務(wù)都是集群部署的烧颖,可以避免單點(diǎn)問題弱左。并且很多緩存服務(wù)都提供了可以用來實(shí)現(xiàn)分布式鎖的方法,比如Tair的put方法炕淮,redis的setnx方法等拆火。并且,這些緩存服務(wù)也都提供了對(duì)數(shù)據(jù)的過期自動(dòng)刪除的支持涂圆,可以直接設(shè)置超時(shí)時(shí)間來控制鎖的釋放们镜。
使用緩存實(shí)現(xiàn)分布式鎖的優(yōu)點(diǎn)
性能好,實(shí)現(xiàn)起來較為方便润歉。
使用緩存實(shí)現(xiàn)分布式鎖的缺點(diǎn)
通過超時(shí)時(shí)間來控制鎖的失效時(shí)間并不是十分的靠譜模狭。
如何設(shè)置的失效時(shí)間太短,方法沒等執(zhí)行完踩衩,鎖就自動(dòng)釋放了嚼鹉,那么就會(huì)產(chǎn)生并發(fā)問題。如果設(shè)置的時(shí)間太長驱富,其他獲取鎖的線程就可能要平白的多等一段時(shí)間锚赤,造成系統(tǒng)吞吐量低,容易導(dǎo)致超時(shí)褐鸥。這個(gè)問題使用數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖同樣存在线脚。