開發中會遇到提工單的時候如果處理點選多次的情況,後端使用redis分散式鎖實現。
選用Redis實現分散式鎖原因
- Redis有很高的效能
- Redis命令對此支援較好,實現起來比較方便
實現思想
- 獲取鎖的時候,使用setnx加鎖,並使用expire命令為鎖新增一個超時時間,超過該時間則自動釋放鎖,鎖的value值為一個隨機生成的UUID,通過此在釋放鎖的時候進行判斷。
- 獲取鎖的時候還設定一個獲取的超時時間,若超過這個時間則放棄獲取鎖。
- 釋放鎖的時候,通過UUID判斷是不是該鎖,若是該鎖,則執行delete進行鎖釋放
分散式鎖的核心程式碼如下:
package com.example.demo.utils;
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;
/**
* Created by liang.liu04@hand-china.com
* on 2018/6/28
*/
public class DistributedLock {
private final JedisPool jedisPool;
public DistributedLock(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
/**
* 加鎖
* @param locaName 鎖的key
* @param acquireTimeout 獲取超時時間
* @param timeout 鎖的超時時間
* @return 鎖標識
*/
public String lockWithTimeout(String locaName,
long acquireTimeout, long timeout) {
Jedis conn = null;
String retIdentifier = null;
try {
// 獲取連線
conn = jedisPool.getResource();
// 隨機生成一個value
String identifier = UUID.randomUUID().toString();
// 鎖名,即key值
String lockKey = "lock:" + locaName;
// 超時時間,上鎖後超過此時間則自動釋放鎖
int lockExpire = (int)(timeout / 1000);
// 獲取鎖的超時時間,超過這個時間則放棄獲取鎖
long end = System.currentTimeMillis() + acquireTimeout;
while (System.currentTimeMillis() < end) {
if (conn.setnx(lockKey, identifier) == 1) {
conn.expire(lockKey, lockExpire);
// 返回value值,用於釋放鎖時間確認
retIdentifier = identifier;
return retIdentifier;
}
// 返回-1代表key沒有設定超時時間,為key設定一個超時時間
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 釋放鎖的標識
* @return
*/
public boolean releaseLock(String lockName, String identifier) {
Jedis conn = null;
String lockKey = "lock:" + lockName;
boolean retFlag = false;
try {
conn = jedisPool.getResource();
while (true) {
// 監視lock,準備開始事務
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;
}
}
測試
下面就用一個簡單的例子測試剛才實現的分散式鎖。
例子中使用50個執行緒模擬秒殺一個商品,使用–運算子來實現商品減少,從結果有序性就可以看出是否為加鎖狀態。
模擬秒殺服務,在其中配置了jedis執行緒池,在初始化的時候傳給分散式鎖,供其使用。
package com.example.demo.service;
import com.example.demo.utils.DistributedLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.exceptions.JedisException;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* Created by 劉亮 on 2017/11/12.
*/
@Service
public class RedisService {
@Autowired
StringRedisTemplate stringRedisTemplate;
@Resource(name = "stringRedisTemplate")
@Autowired
ValueOperations<String, String> valOpsStr;
@Autowired
RedisTemplate<Object, Object> redisTemplate;
@Resource(name = "redisTemplate")
ValueOperations<Object, Object> valOpsObj;
public String getStr(String key) {
return stringRedisTemplate.opsForValue().get(key);//獲取對應key的value
// return valOpsStr.get(key);
}
public void setStr(String key, String val) {
stringRedisTemplate.opsForValue().set(key,val,1800, TimeUnit.SECONDS);
// valOpsStr.set(key, val);
}
public void del(String key) {
stringRedisTemplate.delete(key);
}
/**
* 根據指定o獲取Object
*
* @param o
* @return
*/
public Object getObj(Object o) {
return valOpsObj.get(o);
}
/**
* * 設定obj快取
* * @param o1
* * @param o2
*
*/
public void setObj(Object o1, Object o2) {
valOpsObj.set(o1, o2);
}
/**
* 刪除Obj快取
*
* @param o
*/
public void delObj(Object o) {
redisTemplate.delete(o);
}
private static JedisPool pool = null;
static {
JedisPoolConfig config = new JedisPoolConfig();
// 設定最大連線數
config.setMaxTotal(200);
// 設定最大空閒數
config.setMaxIdle(8);
// 設定最大等待時間
config.setMaxWaitMillis(1000 * 100);
// 在borrow一個jedis例項時,是否需要驗證,若為true,則所有jedis例項均是可用的
config.setTestOnBorrow(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 3000);
}
DistributedLock lock = new DistributedLock(pool);
int n = 500;
public void seckill() {
// 對key為“resource” 的加鎖,實際業務中可以將其改為資料的id, 返回鎖的value值,供釋放鎖時候進行判斷
String indentifier = lock.lockWithTimeout("resource", 5000, 1000);
System.out.println(Thread.currentThread().getName() + "獲得了鎖");
System.out.println(--n);
lock.releaseLock("resource", indentifier);
}
}
模擬執行緒進行秒殺服務
package com.example.demo.test;
import com.example.demo.service.RedisService;
/**
* Created by liang.liu04@hand-china.com
* on 2018/6/28
*/
public class RedisThreadA extends Thread {
private RedisService redisService;
public RedisThreadA(RedisService redisService){
this.redisService = redisService;
}
@Override
public void run() {
redisService.seckill();
}
}
測試類
package com.example.demo.test;
import com.example.demo.service.RedisService;
/**
* Created by liang.liu04@hand-china.com
* on 2018/6/28
*/
public class RedisTestA {
public static void main(String[] args) {
RedisService service = new RedisService();
for (int i = 0; i < 50; i++) {
RedisThreadA threadA = new RedisThreadA(service);
threadA.start();
}
}
}
執行測試類,會發現結果有序執行。