分佈式鎖
概念
- 任何一個系統都無法同時滿足一致性(Consistency),可用性(Availability),分區容錯性(Partition tolerance), 只能同事滿足2個;
- 分佈式鎖就是爲了解決數據一致性問題.
悲觀鎖和樂觀鎖
悲觀鎖:
- 總是假設最壞的情況,每次去拿數據的時候都認爲別人會修改,所以每次拿數據都會上鎖,這樣別人想拿這個數據就會阻塞,知道鎖被釋放
- 悲觀鎖多用於多寫場景,關係型數據庫中很多實用了這種悲觀鎖機制
- 實現:
- redis 實現鎖機制
樂觀鎖
- 總是假設最好的情況,即每次去拿數據的時候都認爲別的線程不會去修改,所以不會上鎖,但是在更新數據的時候會判斷在此期間有沒有其它線程更新了這個數據,可以用版本號機制和CAS算法來實現;
- 樂觀鎖多用戶多讀場景,提高吞吐量,比如數據庫提供的write_condition機制
- 實現:
- 數據庫添加版本號字段: 一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。
- CAS 算法
應用場景
- 涉及到多個實例進程操作同一份數據時就需要用到鎖機制,比如: 下單,修改庫存,更新緩存等
分佈式鎖的特性
- 分佈式環境下同一時刻只能被單個線程獲取;
- 已經獲取鎖的進程在使用中不需要再次獲取;
- 異常或者超時自動釋放鎖,避免死鎖
- 高性能,分佈式環境下必須性能好;
實現方式
- 基於redis 緩存實現;
- 基於zookeeper 臨時順序節點實現;
- 基於數據庫行鎖實現;
redis 分佈式鎖
- redis 配置
spring:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 100
max-idle: 10
min-idle: 5
timeout: 30000
- 分佈式鎖接口 DistributedLock
public interface DistributedLock {
String lock(String name);
String lock(String name, long expire, long timeout);
String tryLock(String name);
String tryLock(String name, long expire);
boolean unlock(String name, String token);
void close();
}
- 分佈式鎖實現 RedisDistributedLockImpl
@Service
@Slf4j
public class RedisDistributedLockImpl implements DistributedLock{
@Autowired
private StringRedisTemplate stringRedisTemplate;
private RedisLock redisLock;
@PostConstruct
public void init(){
redisLock = new RedisLock(stringRedisTemplate);
}
public int getCount() {
return redisLock.getCount();
}
@Override
public String lock(String name){
return redisLock.lock(name);
}
@Override
public String lock(String name, long expire, long timeout){
return redisLock.lock(name, expire, timeout);
}
@Override
public String tryLock(String name) {
return redisLock.tryLock(name);
}
@Override
public String tryLock(String name, long expire) {
return redisLock.tryLock(name, expire);
}
@Override
public boolean unlock(String name, String token) {
return redisLock.unlock(name, token);
}
@Override
public void close() {
redisLock.close();
}
}
- redis 鎖具體邏輯 RedisLock
@Slf4j
public class RedisLock {
private static final String unlockScript =
"if redis.call(\"get\",KEYS[1]) == ARGV[1]\n"
+ "then\n"
+ " return redis.call(\"del\",KEYS[1])\n"
+ "else\n"
+ " return 0\n"
+ "end";
private StringRedisTemplate redisTemplate;
private long timeout = 60000;
private long expire = 300000;
private String PREFIX = "lock:";
private int count = 0;
public RedisLock(StringRedisTemplate redisTemplate) {
init(redisTemplate, this.expire, this.timeout);
}
public RedisLock(StringRedisTemplate redisTemplate, long expire, long timeout) {
init(redisTemplate, expire, timeout);
}
public void init(StringRedisTemplate redisTemplate, long expire, long timeout) {
this.redisTemplate = redisTemplate;
this.expire = expire;
this.timeout = timeout;
}
public int getCount() {
return count;
}
public String lock(String name){
return this.lock(name, this.expire, this.timeout);
}
public String lock(String name, long expire, long timeout){
long startTime = System.currentTimeMillis();
String token;
do{
token = tryLock(name, expire);
log.debug("lock token:{}", token);
if(token == null) {
if((System.currentTimeMillis()-startTime) > (timeout-10))
return token;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}while(token==null);
return token;
}
public String tryLock(String name) {
return this.tryLock(name, this.expire);
}
public String tryLock(String name, long expire) {
String token = UUID.randomUUID().toString();
String key = PREFIX + name;
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
try{
Boolean result = conn.set(key.getBytes(Charset.forName("UTF-8")), token.getBytes(Charset.forName("UTF-8")),
Expiration.from(expire, TimeUnit.MILLISECONDS), RedisStringCommands.SetOption.SET_IF_ABSENT);
if(result!=null && result) {
count++;
return token;
}
} catch (Exception e){
log.error("fail to tryLock name:{}", name);
e.printStackTrace();
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
return null;
}
public boolean unlock(String name, String token) {
String key = PREFIX + name;
byte[][] keysAndArgs = new byte[2][];
keysAndArgs[0] = key.getBytes(Charset.forName("UTF-8"));
keysAndArgs[1] = token.getBytes(Charset.forName("UTF-8"));
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
try {
Long result = (Long)conn.scriptingCommands().eval(unlockScript.getBytes(Charset.forName("UTF-8")), ReturnType.INTEGER, 1, keysAndArgs);
if(result!=null && result>0) {
count--;
return true;
}
} catch (Exception e){
log.error("fail to unlock name:{}, token:{}", key, token);
e.printStackTrace();
} finally {
RedisConnectionUtils.releaseConnection(conn, factory);
}
return false;
}
public void close()
{
log.info("close connect");
RedisConnectionFactory factory = redisTemplate.getConnectionFactory();
RedisConnection conn = factory.getConnection();
RedisConnectionUtils.releaseConnection(conn, factory);
}
}
- redis 分佈式鎖測試
@RunWith(SpringRunner.class)
@SpringBootTest(classes = SosApplication.class)
@Slf4j
public class RedisTest {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private DistributedLock lock;
private Executor executor = Executors.newFixedThreadPool(100);
private int concurrenceResult = 0;
@Before
public void setUp() {
}
@After
public void tearDown() {
}
@Test
public void empty() {
}
@Test
public void deleteRedisOrders() {
while (doDelete("order::*", 5000) == 5000) {
log.info("another loop...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private int doDelete(String match, int n) {
AtomicInteger res = new AtomicInteger();
redisTemplate.execute((RedisCallback<?>) (connection) -> {
int i = 0;
Cursor<byte[]> cursors = null;
log.info("start delete ...");
try {
cursors = connection.scan(ScanOptions.scanOptions().match(match).count(n).build());
while (cursors.hasNext()) {
byte[] key = cursors.next();
connection.del(key);
log.info("delete ---> {}", new String(key));
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
log.info("total deleted {}", i);
res.set(i);
if (cursors != null) {
try {
cursors.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
});
return res.get();
}
@Test
public void testLock() {
String token = lock.lock("test");
log.debug("token:{}", token);
Assert.notNull(token, "lock fail");
boolean rc = lock.unlock("test", token);
Assert.isTrue(rc, "unlock fail");
}
@Test
public void testLockWithConcurrence() {
concurrenceResult = 0;
int threadNum= 30;
int loopNum = 100;
int result = threadNum * loopNum;
CountDownLatch latch = new CountDownLatch(threadNum);
log.info("testThreadLock...");
for(int i=0; i< threadNum; i++) {
executor.execute(() -> {
doWorkWithLock(loopNum, latch);
});
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("testThreadLock result:{}", concurrenceResult);
Assert.isTrue(concurrenceResult==result, "testLockWithConcurrence result should be " + result);
}
private void doWork(int n, CountDownLatch latch){
try {
Random rand = new Random();
for(int i=0; i<n; i++) {
long randNum = rand.nextInt(20);
log.debug("doWork sleep:{}, thread:{}", randNum, Thread.currentThread().getName());
concurrenceResult++;
Thread.sleep(randNum);
}
if(latch != null) {
latch.countDown();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private void doWorkWithLock(int n, CountDownLatch latch){
String name = "lockName";
String token = lock.lock(name);
long s = new Date().getTime();
log.info("doWorkWithLock lock name:{}, token:{}, thread:{}", name, token, Thread.currentThread().getName());
doWork(n, latch);
boolean rc = lock.unlock(name, token);
long e = new Date().getTime();
log.info("doWorkWithLock unlock name:{}, token:{}, rc:{}, times:{}, thread:{}", name, token, rc, e-s, Thread.currentThread().getName());
}
}
- 使用案例
String token = lock.tryLock(LOCK_NAME);
if (token != null) {
try {
} finally {
lock.unlock(LOCK_NAME, token);
}
}