springboot+redis+lua实现分布式锁
1 分布式锁
Java锁能保证一个JVM进程里多个线程交替使用资源。而分布式锁保证多个JVM进程有序交替使用资源,保证数据的完整性和一致性。
分布式锁要求
- 互斥。一个资源在某个时刻只能被一个线程访问。
- 避免死锁。避免某个线程异常情况不释放资源,造成死锁。
- 可重入。
- 高可用。高性能。
- 非阻塞,没获取到锁直接返回失败。
2 实现
1 lua脚本
为了实现redis操作的原子性,使用lua脚本。为了方便改脚本,将脚本单独写在文件里。
-- 加锁脚本
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('pexpire', KEYS[1], ARGV[2]);
return true;
else
return false;
end
-- 解锁脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
redis.call('del', KEYS[1]);
return true;
else
return false;
end
-- 更新锁脚本
if redis.call('get', KEYS[1]) == ARGV[1] then
redis.call('pexpire', KEYS[1], ARGV[2]);
-- pexpire与expire的区别是:pexpire毫秒级,expire秒级
return true;
else
return 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分布式锁业务类
@Service
public class LockService {
private static final long LOCK_EXPIRE = 30_000;
private static final Logger LOGGER = LoggerFactory.getLogger(LockService.class);
@Autowired
private 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 {
@Autowired
private LockService service;
private int count = 0;
@Test
public 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 存在的问题
这个分布式锁实现了互斥,redis键映射资源,如果存在键,则资源正被某个线程持有。如果不存在键,则资源空闲。
避免死锁,靠的是设置reds键的过期时间,同时开启守护线程动态延长redis键的过期时间,直到该线程任务完结。
高性能。redis是内存数据库,性能很高。同时lua脚本使得redis以原子性更新锁状态,避免多次spirngboot与redis的网络IO。
非阻塞。lock()
方法没有获取到锁立即返回false,不会阻塞当前线程。
没有实现可重入和高可用。高可用需要redis集群支持。