1、由来
-
分布式系统多线程、多进程分布在不同的机器上,原来的单机部署情况下的并发策略失效,比如synchronized和ReentrantLock。为了解决此问题需要一种跨JVM的互斥机制来控制共享资源的访问->分布式锁,不仅能锁住同一进程下的不同线程,还能锁住不同进程下的不同线程。
2、概念
-
在分布式架构下,数据只有一份,此时就需要用利用锁的技术控制某个时刻的进程数
-
用一个状态值表示锁,对锁的占用和释放通过状态值标识
3、特点
-
互斥性
-
不仅要在同一JVM进程下的不同线程间互斥,还要在不同JVM进程下的不同线程间互斥
-
-
锁超时
-
支持锁的自动释放,防止死锁
-
-
正确、高效、高可用
-
加锁和解锁必须是同一个线程,加锁和解锁操作一定要高效,提供锁的的服务具备容错性
-
-
可重入
-
若一个线程拿到锁之后继续去获取锁还能获取到,则锁是可重入的(方法的递归调用)
-
-
阻塞/非阻塞
-
若获取不到直接返回视为非阻塞的,若获取不到就一直等待锁的释放或者等待超时的,视为阻塞的
-
-
公平/非公平
-
按照请求的顺序获取锁视为公平的
-
4、高并发超卖问题
@Autowired RedisTemplate<String,String> redisTemplate; String maotai = "maotai20221222"; @PostConstruct public void init(){ redisTemplate.opsForValue().set(maotai,"100"); } @GetMapping("/get/maotai2") public String sale2(){ synchronized (this) { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0) { redisTemplate.opsForValue().set(maotai, String.valueOf(count - 1)); return "success"; } return "fail"; } }
-
synchronized只锁住本地进程下的多个线程
-
在单机环境中能保证数据安全性,但在分布式环境下不能保证
5、SETNX
-
基于redis的SETNX实现分布式锁
-
SETNX key value
-
若key不存在,设置成功
-
若key存在,设置失败
-
-
SETNX返回值
-
设置成功,返回1
-
设置失败,返回0
-
-
SETNX实现同步锁的流程
-
使用SETNX获取锁,若返回0(key已存在,锁存在)则获取失败,反之获取成功
-
为防止获取锁后程序出现异常,导致其他线程/进程调用SETNX返回0而进入死锁状态,需要为该key设置合理TTL
-
使用DEL释放锁
-
String lockey = "maotailock"; @GetMapping("/get/maotai3") public String sale3(){ // Boolean isLock = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { // redisConnection.setNX(lock.getBytes(),"1".getBytes()); // return null; // }); Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey, "1");//底层调用setnx if (isLock){ redisTemplate.expire(lockey,5, TimeUnit.SECONDS); try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0){ redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1)); return "success"; } return "fali"; }catch (Exception e){ e.printStackTrace(); }finally { redisTemplate.delete(lockey); } } return "Don't get lock"; }
-
问题
-
setnx和设置超时时间是非原子性操作,为保证原子性需要使用lua脚本或者设置值的同时设置TTL
-
错误解锁,加锁和解锁必须是同一个线程
-
6、使用lua脚本
@GetMapping("/get/maotai4") public String sale4(){ String lockLua = "" + "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 " + "then redis.call('expire',KEYS[1],ARGV[2]);" + "return true else return false " + "end"; redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.eval( lockLua.getBytes(), //要执行的lua脚本 ReturnType.BOOLEAN, //lua脚本返回值类型 1, //lua脚本中涉及的key的数量 lockey.getBytes(), //KEYS[1]对应的值 "1".getBytes(), //ARGV[1]对应的值 "5".getBytes() //ARGV[2]对应的值 )); if (isLock){ try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0){ redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1)); return "success"; } return "fail"; }catch (Exception e){ e.printStackTrace(); }finally { //释放锁 redisTemplate.delete(maotai); } } return "Don't get lock"; }
-
问题
-
若设置的TTL不合理,线程A业务执行时间超过TTL,其他线程会进来操作共享变量,导致数据不安全
-
若线程A达到TTL时会释放线程B的锁,导致错误释放
-
7、SETNX的同时设置TTL
@GetMapping("/get/maotai5") public String sale5(){ Boolean isLock =redisTemplate.opsForValue().setIfAbsent(lockey,"1",5,TimeUnit.SECONDS); if (isLock){ try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0){ redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1)); return "success"; } return "fail"; }catch (Exception e){ e.printStackTrace(); }finally { redisTemplate.delete(lockey); } } return "Don't get lock"; }
-
虽然保证原子性,但设置的TTL不合理也会导致错误解锁
8、为锁加唯一标识
@GetMapping("/get/maotai6") public String sale6(){ String requestId = UUID.randomUUID().toString() + Thread.currentThread().getId(); Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey,requestId,5,TimeUnit.SECONDS); if (isLock){ try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0){ redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1)); return "success"; } return "fail"; }catch (Exception e){ e.printStackTrace(); }finally { String id = redisTemplate.opsForValue().get(lockey); //判断是自己的锁才能去释放 if (id != null && id.equals(requestId)){ redisTemplate.delete(lockey); } } } return "Don't get lock"; }
-
若线程A在finally中得到key之后刚好达到TTL,此时线程B进来并获取到锁,然后线程A执行DEL会将线程B的锁释放,错误解锁
9、解锁时使用lua脚本
@GetMapping("/get/maotai7") public String sale7(){ String requestId = UUID.randomUUID().toString() + Thread.currentThread().getId(); Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey,requestId,5,TimeUnit.SECONDS); if (isLock){ try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0){ redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1)); return "success"; } return "fail"; }catch (Exception e){ e.printStackTrace(); }finally { String unlockLua = "" + "if redis.call('get',KEY[1]) == ARGV[1]" + "then redis.call('del',KEY[1]);" + "return true else return false" + "end"; redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.eval( unlockLua.getBytes(), //lua脚本 ReturnType.BOOLEAN, //lua脚本返回值类型 1, //lua脚本中涉及的key的数量 lockey.getBytes(), //KEY[1] requestId.getBytes() //ARGV[1] )); } } return "Don't get lock"; }
10、锁续期
-
拿到锁之后执行业务,若业务的执行时间超过了锁的过期时间,则给锁续期
-
给拿到锁的线程创建一个守护线程,守护线程定时判断拿到锁的线程是否还持有锁,若持有锁则为其续期
-
ScheduledExecutorService executorService;//创建守护线程 ConcurrentSkipListSet<String> set = new ConcurrentSkipListSet<>();//队列 @PostConstruct public void init2(){ executorService = Executors.newScheduledThreadPool(1); //续期的lua脚本 String renewalLua = "" + "if redis.call('get',KEYS[1] == ARGV[1]" + "then redis.call('expire',KEYS[1],ARGV[2]);" + "return true else return false" + "end)"; executorService.scheduleAtFixedRate(() -> { Iterator<String> iterator = set.iterator(); while (iterator.hasNext()){ String requestId = iterator.next(); redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> { Boolean eval = false; try { eval = redisConnection.eval( renewalLua.getBytes(), ReturnType.BOOLEAN, 1, lockey.getBytes(), requestId.getBytes(), "5".getBytes() ); }catch (Exception e){ log.info("锁续期失败,{}",e.getMessage()); } return eval; }); } },0,1,TimeUnit.SECONDS); } @GetMapping("/get/maotai8") public String sale8() { String requestId = UUID.randomUUID().toString() + Thread.currentThread().getId(); Boolean isLock = redisTemplate.opsForValue().setIfAbsent(lockey, requestId, 5, TimeUnit.SECONDS); if (isLock){ //若获取成功后让守护线程为其续期 set.add(requestId); try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0){ redisTemplate.opsForValue().set(maotai,String.valueOf(count - 1)); sale8();//递归调用,锁可重入 TimeUnit.SECONDS.sleep(10); return "success"; } return "fail"; }catch (Exception e){ e.printStackTrace(); }finally { //解锁锁续期 set.remove(requestId); //释放锁 String unlockLua = "" + "if redis.call('get',KEY[1]) == ARGV[1]" + "then redis.call('del',KEY[1]);" + "return true else return false" + "end"; redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> redisConnection.eval( unlockLua.getBytes(), ReturnType.BOOLEAN, 1, lockey.getBytes(), requestId.getBytes() )); } } return "Don't get lock"; }
11、锁的可重入/阻塞锁
-
加锁的次数和解锁的次数要一致,使用hash数据类型,记录重入次数
-
之前实现的都是非阻塞锁,若获取不到锁就返回了;阻塞锁:获取不到锁就等待锁的释放,知道获取到锁或者等待超时
-
基于客户端轮询方案
-
每隔一定时间就尝试获取锁,浪费资源
-
-
基于redis发布/订阅方案
-
线程A获取锁并设置TTL后,线程B获取锁失败,然后订阅线程A释放锁的消息,线程B处于阻塞等待状态,知道等到线程A释放锁
-
-
基于Redisson
-
Redisson内置了一系列的分布式对象,分布式集合,分布式锁,分布式服务等诸多功能特性,是一款基
于Redis实现,拥有一系列分布式系统功能特性的工具包
@Value("${spring.redis.host}") String host; @Value("${spring.redis.port}") String port; @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer().setAddress("redis://"+host+":"+port); return Redisson.create(config); } @Autowired private RedissonClient redissonClient; @GetMapping("/get/maotai9") public String sale9(){ //要去获取锁 RLock lock = redissonClient.getLock(lockey); //获取到锁 lock.lock(); try { Integer count = Integer.parseInt(redisTemplate.opsForValue().get(maotai)); if (count > 0) { redisTemplate.opsForValue().set(maotai,String.valueOf(count-1)); return "success"; } return "fail"; }catch (Exception e){ e.printStackTrace(); }finally { //释放锁 lock.unlock(); } return ""; }
-
-