目录
一、定义与原理
基于Redis的分布式锁
获取锁
释放锁
锁误删问题:因为key值一样,将别人的锁删掉了
锁误判问题二:判断锁和释放锁不是原子性的
Lua脚本
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
分布式锁的优点:
- 多进程可见
- 高可用
- 安全性
- 高性能
- 互斥
知识点补充:
多进程(Multiprocessing)是操作系统中的一个重要概念,它指的是在操作系统中同时运行多个程序实例的能力。这些程序实例,即进程(Process),是系统进行资源分配和调度的一个独立单元,是程序在计算机上的一次执行活动。每个进程都有自己独立的内存空间和系统资源,它们之间通过特定的机制(如管道、消息队列、共享内存等)进行通信和数据交换。
多线程(Multithreading)是指从软件或者硬件上实现多个线程并发执行的技术。在计算机编程中,多线程是一种强大的工具,它允许程序能够同时执行多个任务,从而提高程序的执行效率和响应性。以下是对多线程的详细解释:
一、定义与原理
- 定义:多线程是并行化的一种形式,通过拆分工作以便同时进行处理。在程序中,这些独立运行的程序片段被称作“线程”(Thread),利用它编程的概念就叫作“多线程处理”。
- 原理:多线程的并发执行机制原理是将一个处理器划分为若干个短的时间片,每个时间片依次轮流地执行处理各个线程,由于时间片非常短,相对于一个应用程序来说,就好像是处理器在为自己单独服务一样,从而达到多个应用程序或线程在同时进行的效果。
基于Redis的分布式锁
实现分布式锁时有两个要实现的基本方法:
获取锁
- 互斥:确保只能有一个线程获取锁
- 非阻塞:尝试一次,成功返回true,失败返回false
- set lock thread ex 10 nx
- 获取锁时存入线程标示(可以用UUID标示,UUID+ThreadId)
细节:如果第一步 setnx lock thread
第二部 expire lock 10
那么,如果redis在第一步执行完第二步执行前宕机了,就有造成死锁的概率
释放锁
- 手动释放
- 超时释放:获取锁时加一个超时时间
- DEL KEY
- 在释放锁时先获取锁中的线程标示,判断是否与当前的线程标示一致
一致释放锁,不一致不释放
锁误删问题:因为key值一样,将别人的锁删掉了
问题:线程1业务阻塞,redis锁超时已经释放,线程2拿到锁,但线程1去手动释放锁时,因为key一样,把线程2的锁给释放了,导致线程3拿到锁,同时就有两个线程拿到了锁。
解决:线程在手动释放锁的时候,判断一下这是不是自己上的锁,key的value值设为标示,UUID+ThreadId
、
ID_PREFIX
是一个静态的UUID前缀,这意味着它对于类来说是唯一的,但在多个JVM实例之间不是唯一的。不过,在这个上下文中,它主要用于与线程ID结合,以形成一个更长的、理论上唯一的标识符。Thread.currentThread().getId()
获取当前线程的ID。这个ID在JVM内部是唯一的,但跨JVM则不是。
线程id:是JVM在创建线程时赋值的id,id是自增的,不同JVM的线程id可能相同
在这里锁对应的值不相同,因为值的前缀有UUID,不同的JVM不同的线程进来,都会创建锁对象上锁,每个锁对象在上锁时,都会生成一个随机的UUID作为value的前缀,所以key对应的值不同,id标示不同,就只能释放自己的锁。
未改进代码
public class SimpleRedisLock implements ILock{private String name;// 锁名称private final String KEY_PREFIX="lock:";private final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";private StringRedisTemplate stringRedisTemplate;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}@Overridepublic boolean tryLock(long timeoutSec) {// 给线程id加个uuid前缀,避免在不同jvm下线程id相同,导致值相同String threadId = ID_PREFIX+Thread.currentThread().getId();Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(result);}@Overridepublic void unLock() {String threadId = ID_PREFIX+Thread.currentThread().getId();String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);if (threadId.equals(id)){stringRedisTemplate.delete(KEY_PREFIX+name);}}
}
锁误判问题二:判断锁和释放锁不是原子性的
在释放锁时,线程1已经判断完了,要去释放锁,但JVM垃圾回收时,遇到了阻塞,时间过久后,锁超时释放,线程2拿到锁开始执行业务,这时,线程1恢复正常,因为已经判断过锁是我上的了,它还会去释放锁,锁的key值都是 ->lock:业务:用户id,它找到线程2上的锁,并释放,线程3就可以来拿到锁,开始执行业务
Lua脚本
保证判断锁和释放锁原子性措施:
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言
Redis提供的调用函数,语法如下:
redis.cail('命令名称','key','其它参数',...)
带参数的Lua语法
改进后的上锁和解锁
public class SimpleRedisLock implements ILock{private String name;// 锁名称private StringRedisTemplate stringRedisTemplate;private static final String KEY_PREFIX="lock:";// 不同jvm来加载这个类时,创建的uuid前缀不同,但这个uuid是静态的,无论对象创建多少次,都是唯一确定的private static final String ID_PREFIX= UUID.randomUUID().toString(true)+"-";private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {this.name = name;this.stringRedisTemplate = stringRedisTemplate;}static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Overridepublic boolean tryLock(long timeoutSec) {// 给线程id加个uuid前缀,避免在不同jvm下线程id相同,导致值相同String threadId = ID_PREFIX+Thread.currentThread().getId();Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(result);}@Overridepublic void unLock() {// 执行lua脚本,来确保判断id标示和执行释放锁的命令的原子性stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX+Thread.currentThread().getId());}
}