Redis分布式锁
什么是Redis
在我们日常的Java Web开发中,无不都是使用数据库来进行数据的存储,由于一般的系统任务中通常不会存在高并发的情况,所以这样看起来并没有什么问题,可是一旦涉及大数据量的需求,比如一些商品抢购的情景,或者是主页访问量瞬间较大的时候,单一使用数据库来保存数据的系统会因为面向磁盘,磁盘读/写速度比较慢的问题而存在严重的性能弊端,一瞬间成千上万的请求到来,需要系统在极短的时间内完成成千上万次的读/写操作,这个时候往往不是数据库能够承受的,极其容易造成数据库系统瘫痪,最终导致服务宕机的严重生产问题。
NoSQL技术
为了克服上述的问题,Java Web项目通常会引入NoSQL技术,这是一种基于内存的数据库,并且提供一定的持久化功能。
Redis和MongoDB是当前使用最广泛的NoSQL,而就Redis技术而言,它的性能十分优越,可以支持每秒十几万此的读/写操作,其性能远超数据库,并且还支持集群、分布式、主从同步等配置,原则上可以无限扩展,让更多的数据存储在内存中,更让人欣慰的是它还支持一定的事务能力,这保证了高并发的场景下数据的安全和一致性。
跨JVM的线程安全问题
在单体的应用开发场景中,在多线程的环境下,涉及并发同步的时候,为了保证一个代码块在同一时间只能由一个线程访问。我们一般可以使用synchronized和ReetrantLock去保证,这实际上就是本地锁的方式。也就是说在同一个JVM内部,往往采用synchronized或者Lock的方式来解决多线程的问题。但是在分布式集群工作的开发场景中,在JVM之间,就需要分布式锁来处理跨JVM之间的线程安全问题。
本地锁
在单进程的系统中,当存在多个线程可以同时改变某个变量时,就需要对变量或者代码块做同步,使其在修改这种变量时能够线性执行,以防止并发修改变量带来数据不一致或者数据污染的现象。为了实现多个线程在同一时刻同一个代码块只能由一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续进行发现已经有标记了则等待拥有标记的线程结束同步代码块,取消标记之后再去尝试设置标记。这个标记可以理解为锁。
分布式锁
如果是单机情况下(单JVM),线程之间共享内存,只要线程锁就可以解决并发问题。单如果分布式的情况下(多JVM),线程A和线程B可能不在同一个JVM中,这样线程锁就无法起作用了,这时候就需要使用分布式锁来解决。
对于分布式场景,我们可以使用分布式锁,它是控制分布式系统之间互斥访问共享资源的一种方式。
比如说在一个分布式系统中,多台机器上部署了多个服务,当客户端一个用户重复发起数据插入请求时,如果没有分布式锁机制保证,那么那多台机器上的多个服务可能进行并发插入操作,导致数据重复插入,对于某些不允许有多余数据的业务来说,这就会造成问题。而分布式锁机制就是为了解决类似这类问题,保证多个服务之间互斥的访问共享资源,如果一个服务抢占了分布式锁,其他服务没获取到锁,就不进行后续操作。
分布式锁条件
- 互斥性:在任意时刻,只有一个客户端能持有锁
- 不会发生死锁:即使有一个客户端在持有锁期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁
- 具有兼容性:只要大部分Redis节点正常运行,客户端就可以加锁和解锁
- 解铃还须系铃人:加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了
Redis分布式锁
Redis几种架构:单机模式、主从模式、哨兵模式、集群模式
Redis分布式锁机制,主要借助setnx和expire两个命令完成
setnx命令:
SETNX是SET if Not eXists的简写。当且仅当key不存在时,将key的值设为value;若给定的key已经存在,则SETNX不会做任何动作
127.0.0.1:6379> set lock "unlock" OK 127.0.0.1:6379> setnx lock "unlock" (integer) 0 127.0.0.1:6379> setnx lock "lock" (integer) 0 127.0.0.1:6379>
expire命令
expire命令为key设置生存时间,当key生存时间过期时(生存时间为0),它会被自动删除
127.0.0.1:6379> expire lock 10 (integer) 1 127.0.0.1:6379> ttl lock 8
通过Redis的setnx、expire命令实现简单的锁机制:
- key不存在时创建,并设置value和过期时间,返回值为1;成功获取锁
- 如果key存在之间返回0,抢锁失败
- 持有锁的线程释放锁时,手动删除key;或过期时间到,自动删除key,锁释放。
线程调用setnx方法返回1之后,认为加锁成功,其他线程要等到持有锁线程业务完成之后释放锁之后,才能再次调用setnx方法加锁
还有一个问题:假设在一个分布式环境下,多个服务器实例请求获取锁,其中服务实例1成功抢夺到了锁,在执行业务时,服务器实例突然挂了,那么这个时候锁会不会释放?什么时候释放?我们第一次加锁的时候使用expire给锁key设置了生存时间,默认时30秒,由此看来如果服务器实例挂了,锁最终也会释放,其他服务器实例还可以继续获取到锁执行业务。但是如果30秒之后,服务器1没有挂但是业务还没有执行完,这个时候锁被释放了就会导致线程问题,这个问题就可以使用watch dog
watch dog自动延期机制
Redisson采取watch dog自动延期机制,它是这样的逻辑:只要服务器实例1加锁成功了,就会启动一个watch dog看门狗,他是一个后台线程,每隔十秒会检查一下,如果服务器实例还持有锁key,那么就会不断的延长锁key的生存时间
Redisson简介
Redisson是一个在Redis的基础上实现的Java驻内存数据网格(In-Memory Data Grid)。它不仅提供了一系列的分布式的Java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock、联锁(MultiLock)、 红锁(RedLock)、 读写锁(ReadWriteLock)等,还提供了许多分布式服务。Redisson提供了使用Redis的最简单和最便捷的方法。Redisson的宗旨是促进使用者对Redis的关注分离(Separation of Concern),从而让使用者能够将精力更集中地放在处理业务逻辑上。
功能和特性
-
支持 Redis 单节点(single)模式、哨兵(sentinel)模式、主从(Master/Slave)模式以及集群(Redis Cluster)模式
-
程序接口调用方式采用异步执行和异步流执行两种方式
-
数据序列化,Redisson 的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在 Redis 里的读取和存储
-
单个集合数据分片,在集群模式下,Redisson 为单个 Redis 集合类型提供了自动分片的功能
-
提供多种分布式对象,如:Object Bucket,Bitset,AtomicLong,Bloom Filter 和 HyperLogLog 等
-
提供丰富的分布式集合,如:Map,Multimap,Set,SortedSet,List,Deque,Queue 等
-
分布式锁和同步器的实现,可重入锁(Reentrant Lock),公平锁(Fair Lock),联锁(MultiLock),红锁(Red Lock),信号量(Semaphonre),可过期性信号锁(PermitExpirableSemaphore)等
-
提供先进的分布式服务,如分布式远程服务(Remote Service),分布式实时对象(Live Object)服务,分布式执行服务(Executor Service),分布式调度任务服务(Schedule Service)和分布式映射归纳服务(MapReduce)
在Redission框架中大量的使用了Lua脚本
Lua脚本
Lua是一种开源、简单易学、轻量小巧的脚本语言,用标准C语言编写。
其设计的目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Redis从2.6版本开始支持Lua脚本,Redis使用Lua可以:
- 原子操作。Redis会将整个脚本作为一个整体执行,不会被中断。可以用来批量更新、批量插入
- 减少网络开销。多个Redis操作合并为一个脚本,减少网络时延
- 代码复用。客户端发送的脚本可以存储在Redis中,其他客户端可以根据脚本的id调用。
Redisson的使用
引入依赖,使用maven在pom.xml中引入
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.15.6</version>
</dependency>
创建配置类,链接Redis
创建Redissong配置类,配置redis地址、密码等信息,连接Redis
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
*
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redissonClient() {
// 创建配置 指定redis地址及节点信息
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(1).setPassword("123456");
// 根据config创建出RedissonClient实例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
到这环境就搭建好了,接下来就是使用Redisson分布式锁。
获取锁
RLock rLock = redissonClient.getLock(lockKey);
加锁
rLock.lock();
释放锁
rLock.unlock();
配置RedissonConfig
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
application.yml配置
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 5000
编写RedissonUtils
package com.example.utils;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* @author Trellis
* @since 2024/03/15 20:10
*/
@Component
public class RedissonLockUtils implements ApplicationContextAware {
protected static ApplicationContext applicationContext;
public void lock(String lockKey, int timeout) {
RedissonClient redissonClient = (RedissonClient)applicationContext.getBean("redissonClient");
RLock lock = redissonClient.getLock(lockKey);
lock.lock(timeout, TimeUnit.SECONDS);
}
public void unlock(String lockKey) {
RedissonClient redissonClient = (RedissonClient)applicationContext.getBean("redissonClient");
RLock lock = redissonClient.getLock(lockKey);
if(lock.isHeldByCurrentThread())
lock.unlock();
}
@Override
public void setApplicationContext(ApplicationContext arg0) throws BeansException {
if (applicationContext == null) {
applicationContext = arg0;
}
}
}
业务处理
/**
* 新增
*/
public void add(Address address) {
try {
redissonLockUtils.lock("addressAdd",10);
//成功获得锁,处理业务
addressMapper.insert(address);
}finally {
//最后要解锁
redissonLockUtils.unlock("addressAdd");
}
}