当前位置: 首页 > news >正文

springboot+redis+lua实现分布式锁

1 分布式锁

Java锁能保证一个JVM进程里多个线程交替使用资源。而分布式锁保证多个JVM进程有序交替使用资源,保证数据的完整性和一致性。
分布式锁要求

  1. 互斥。一个资源在某个时刻只能被一个线程访问。
  2. 避免死锁。避免某个线程异常情况不释放资源,造成死锁。
  3. 可重入。
  4. 高可用。高性能。
  5. 非阻塞,没获取到锁直接返回失败。

2 实现

1 lua脚本

为了实现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的区别是: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分布式锁业务类

@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 存在的问题

这个分布式锁实现了互斥,redis键映射资源,如果存在键,则资源正被某个线程持有。如果不存在键,则资源空闲。
避免死锁,靠的是设置reds键的过期时间,同时开启守护线程动态延长redis键的过期时间,直到该线程任务完结。
高性能。redis是内存数据库,性能很高。同时lua脚本使得redis以原子性更新锁状态,避免多次spirngboot与redis的网络IO。
非阻塞。lock()方法没有获取到锁立即返回false,不会阻塞当前线程。

没有实现可重入和高可用。高可用需要redis集群支持。

http://www.lryc.cn/news/492747.html

相关文章:

  • 【Petri网导论学习笔记】Petri网导论入门学习(十一) —— 3.3 变迁发生序列与Petri网语言
  • docker-compose文件的简介及使用
  • [护网杯 2018]easy_tornado
  • 基于STM32的智能风扇控制系统
  • 决策树——基于乳腺癌数据集与cpu数据集实现
  • 探索空间自相关:揭示地理数据中的隐藏模式
  • echarts使用示例
  • Flink高可用配置(HA)
  • 如何编写出色的技术文档
  • 学习日记_20241126_聚类方法(谱聚类Spectral Clustering)
  • 图书系统小案例
  • 目标检测之学习路线(本科版)
  • C#调用C++ DLL方法之C++/CLI(托管C++)
  • 免费搭建一个属于自己的个性化博客(Hexo+Fluid+Github)
  • vue3 开发利器——unplugin-auto-import
  • 开发需求总结19-vue 根据后端返回一年的数据,过滤出符合条件数据
  • 人工智能如何改变创新和创造力?
  • Github 基本使用学习笔记
  • 群论入门笔记
  • 2024最新python使用yt-dlp
  • Python + 深度学习从 0 到 1(00 / 99)
  • 单点登录深入详解之设计方案总结
  • Loadsh源码分析-forEach,eachRight,map,flatMap,flatMapDeep,flatMapDepth
  • 检测到“runtimelibrary”的不匹配项: 值“mtd_staticdebug”不匹配值“mdd_dynamic”
  • go clean -modcache命令清理缓存
  • C#结构体排序(数组)
  • 基于边缘智能网关的机房安全监测应用
  • 【Jenkins】自动化部署 maven 项目笔记
  • LeetCode 3243. Shortest Distance After Road Addition Queries I
  • ML 系列:第 31 节— 机器学习中的协方差和相关性