網(wǎng)站空間 價格網(wǎng)站流量數(shù)據(jù)
1 分布式鎖
Java鎖能保證一個JVM進程里多個線程交替使用資源。而分布式鎖保證多個JVM進程有序交替使用資源,保證數(shù)據(jù)的完整性和一致性。
分布式鎖要求
- 互斥。一個資源在某個時刻只能被一個線程訪問。
- 避免死鎖。避免某個線程異常情況不釋放資源,造成死鎖。
- 可重入。
- 高可用。高性能。
- 非阻塞,沒獲取到鎖直接返回失敗。
2 實現(xiàn)
1 lua腳本
為了實現(xiàn)redis操作的原子性,使用lua腳本。為了方便改腳本,將腳本單獨寫在文件里。
-- 加鎖腳本
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 thenredis.call('pexpire', KEYS[1], ARGV[2]);return true;
elsereturn false;
end-- 解鎖腳本
if redis.call('get', KEYS[1]) == ARGV[1] thenredis.call('del', KEYS[1]);return true;
elsereturn false;
end-- 更新鎖腳本
if redis.call('get', KEYS[1]) == ARGV[1] thenredis.call('pexpire', KEYS[1], ARGV[2]);-- pexpire與expire的區(qū)別是:pexpire毫秒級,expire秒級return true;
elsereturn false;
end
將腳本裝在Springboot容器管理的bean里。
@Configuration
public class RedisConfig {@Bean("lock")public RedisScript<Boolean> lockRedisScript() {DefaultRedisScript redisScript = new DefaultRedisScript<>();redisScript.setResultType(Boolean.class);redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/ratelimit/lock.lua")));return redisScript;}@Bean("unlock")public RedisScript<Boolean> unlockRedisScript() {DefaultRedisScript redisScript = new DefaultRedisScript<>();redisScript.setResultType(Boolean.class);redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/ratelimit/unlock.lua")));return redisScript;}@Bean("refresh")public RedisScript<Boolean> refreshRedisScript() {DefaultRedisScript redisScript = new DefaultRedisScript<>();redisScript.setResultType(Boolean.class);redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("/ratelimit/refresh.lua")));return redisScript;}
}
redis分布式鎖業(yè)務類
@Service
public class LockService {private static final long LOCK_EXPIRE = 30_000;private static final Logger LOGGER = LoggerFactory.getLogger(LockService.class);@Autowiredprivate RedisTemplate<String, Object> redisTemplate;@Autowired@Qualifier("lock")private RedisScript<Boolean> lockScript;@Autowired@Qualifier("unlock")private RedisScript<Boolean> unlockScript;@Autowired@Qualifier("refresh")private RedisScript<Boolean> refreshScript;public boolean lock(String key, String value) {boolean res = redisTemplate.execute(lockScript, List.of(key), value, LOCK_EXPIRE);if (res == false) {return false;}refresh(key, value);LOGGER.info("lock, key: {}, value: {}, res: {}", key, value, res);return res;}public boolean unlock(String key, String value) {Boolean res = redisTemplate.execute(unlockScript, List.of(key), value);LOGGER.info("unlock, key: {}, value: {}, res: {}", key, value, res);return res != null && Boolean.TRUE.equals(res);}private void refresh(String key, String value) {Thread t = new Thread(() -> {while (true) {redisTemplate.execute(refreshScript, List.of(key), value, LOCK_EXPIRE);try {Thread.sleep(LOCK_EXPIRE / 2);} catch (InterruptedException e) {e.printStackTrace();}LOGGER.info("refresh, current time: {}, key: {}, value: {}", System.currentTimeMillis(), key, value);}});t.setDaemon(true); // 守護線程t.start();}
}
測試類
@SpringBootTest(classes = DemoApplication.class)
public class LockServiceTest {@Autowiredprivate LockService service;private int count = 0;@Testpublic void test() throws Exception {List<CompletableFuture<Void>> taskList = new ArrayList<>();for (int threadIndex = 0; threadIndex < 10; threadIndex++) {CompletableFuture<Void> task = CompletableFuture.runAsync(() -> addCount());taskList.add(task);}CompletableFuture.allOf(taskList.toArray(new CompletableFuture[0])).join();}public void addCount() {String id = UUID.randomUUID().toString().replace("-", "");boolean tryLock = service.lock("account", id);while (!tryLock) {tryLock = service.lock("account", id);}for (int i = 0; i < 10_000; i++) {count++;}try {Thread.sleep(100_000);} catch (Exception e) {System.out.println(e);}for (int i = 0; i < 3; i++) {boolean releaseLock = service.unlock("account", id);if (releaseLock) {break;}}}
}
3 存在的問題
這個分布式鎖實現(xiàn)了互斥,redis鍵映射資源,如果存在鍵,則資源正被某個線程持有。如果不存在鍵,則資源空閑。
避免死鎖,靠的是設(shè)置reds鍵的過期時間,同時開啟守護線程動態(tài)延長redis鍵的過期時間,直到該線程任務完結(jié)。
高性能。redis是內(nèi)存數(shù)據(jù)庫,性能很高。同時lua腳本使得redis以原子性更新鎖狀態(tài),避免多次spirngboot與redis的網(wǎng)絡IO。
非阻塞。lock()
方法沒有獲取到鎖立即返回false,不會阻塞當前線程。
沒有實現(xiàn)可重入和高可用。高可用需要redis集群支持。