Redis分布式锁(二)之Redisson实现
Redisson
是一个用于连接redis
的java
客户端,相对于jedis
,是一个采用异步模型,大量基于netty promise
编程实现的客户端框架。是更高性能的第三方库。所以,这里推荐大家使用Redission
替代jedis
。
一、简介
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),信号量(Semaphore),可过期性信号锁(PermitExpirableSemaphore)等
- 提供先进的分布式服务,如分布式远程服务(Remote Service),分布式实时对象(Live Object)服务,分布式执行服务(Executor Service),分布式调度任务服务(Schedule Service)和分布式映射归纳服务(MapReduce)
二、使用
2.1 客户端
获取RedissonClient对象。RedissonClient
有多种模式,主要的模式有:
- 单节点模式
- 哨兵模式
- 主从模式
- 集群模式
2.1.1 单节点模式
单节点模式的程序化配置方法,大致如下:
Config config = new Config();
config.useSingleServer().setAddress("redis://myredisserver:6379");
RedissonClient redisson = Redisson.create(config);
SingleServerConfig singleConfig = config.useSingleServer();
配置参数可以参考SingleServerConfig参数
2.1.2 哨兵模式
哨兵模式即sentinel
模式,配置Redis
哨兵服务的官方文档在这里。
哨兵模式实现代码和单机模式几乎一样,唯一的不同就是
Config
的构造.
程序化配置哨兵模式的方法如下:
Config config = new Config();
config.useSentinelServers()
.setMasterName("mymaster")
// use "rediss://" for SSL connection
.addSentinelAddress("redis://127.0.0.1:26389", "redis://127.0.0.1:26379")
.addSentinelAddress("redis://127.0.0.1:26319");
RedissonClient redisson = Redisson.create(config);
Redisson的哨兵模式的使用方法如下:
SentinelServersConfig sentinelConfig = config.useSentinelServers();
配置参数可以参考SentinelServersConfig参数
2.1.3 主从模式
介绍配置Redis
主从服务组态的文档在这里.
程序化配置主从模式的方法如下:
Config config = new Config();
config.useMasterSlaveServers()
// use "rediss://" for SSL connection
.setMasterAddress("redis://127.0.0.1:6379")
.addSlaveAddress("redis://127.0.0.1:6389", "redis://127.0.0.1:6332", "redis://127.0.0.1:6419")
.addSlaveAddress("redis://127.0.0.1:6399");
RedissonClient redisson = Redisson.create(config);
主从模式使用到MasterSlaveServersConfig:
MasterSlaveServersConfig masterSlaveConfig = config.useMasterSlaveServers();
配置参数可以参考MasterSlaveServersConfig参数
2.1.4 集群模式
集群模式除了适用于Redis
集群环境,也适用于任何云计算服务商提供的集群模式,例如AWS ElastiCache集群版、Azure Redis Cache和阿里云(Aliyun)的云数据库Redis版。介绍配置Redis集群组态的文档在这里。Redis集群组态的最低要求是必须有三个主节点。
集群模式构造Config
如下:
Config config = new Config();
config.useClusterServers()
.setScanInterval(2000) // 集群状态扫描间隔时间,单位是毫秒
// 可以用"rediss://"来启用SSL连接
.addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
.addNodeAddress("redis://127.0.0.1:7002");
RedissonClient redisson = Redisson.create(config);
集群模式使用到ClusterServersConfig:
ClusterServersConfig clusterConfig = config.useClusterServers();
配置参数可以参考ClusterServersConfig参数
2.2 SpringBoot整合Redisson
Redisson
有多种模式,首先介绍单机模式的整合。
2.2.1 导入Maven依赖
<!-- redisson-springboot -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.16.0</version>
</dependency>
2.2.2 核心配置文件
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 5000
2.2.3 添加配置类
RedissonConfig.java
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
String redisUrl = String.format("redis://%s:%s", redisProperties.getHost() + "",
redisProperties.getPort() + "");
config.useSingleServer().setAddress(redisUrl).setPassword(redisProperties.getPassword());
config.useSingleServer().setDatabase(3);
return Redisson.create(config);
}
}
2.2.4 自定义starter
由于redission
可以有多种模式,将多种模式封装成一个start
。封装一个RedissonManager
,通过策略模式,根据不同的配置类型,创建RedissonConfig
实例,然后创建RedissonClient
对象。具体参考redission-starter
三、Redisson数据结构使用
3.1 使用RBucket操作分布式对象
Redission
模拟了Java
的面向对象编程思想,可以简单理解为一切皆为对象。每一个Redisson
对象实现了RObject和RExpirable两个interfaces。
Usage example:
RObject object = redisson.get...()
object.sizeInMemory();
object.delete();
object.rename("newname");
object.isExists();
// catch expired event
object.addListener(new ExpiredObjectListener() {
// ...
});
// catch delete event
object.addListener(new DeletedObjectListener() {
// ...
});
每一个Redisson对象的名字,就是Redis中的Key.
RMap map = redisson.getMap("mymap");
map.getName(); // = mymap
可以通过RKeys接口操作Redis中的keys.
Usage example:
RKeys keys = redisson.getKeys();
Iterable<String> allKeys = keys.getKeys();
Iterable<String> foundedKeys = keys.getKeysByPattern('key*');
long numOfDeletedKeys = keys.delete("obj1", "obj2", "obj3");
long deletedKeysAmount = keys.deleteByPattern("test?");
String randomKey = keys.randomKey();
long keysAmount = keys.count();
keys.flushall();
keys.flushdb();
Redisson通过RBucket接口代表可以访问任何类型的基础对象,或者普通对象。
RBucket有一系列的工具方法,如compareAndSet(),get(),getAndDelete(),getAndSet(),set(),size(),trySet()等等,用于设值/取值/获取尺寸。
RBucket普通对象的最大大小,为512兆字节。
RBucket<AnyObject> bucket = redisson.getBucket("anyObject");
bucket.set(new AnyObject(1));
AnyObject obj = bucket.get();
bucket.trySet(new AnyObject(3));
bucket.compareAndSet(new AnyObject(4), new AnyObject(5));
bucket.getAndSet(new AnyObject(6));
下面是一个完整的实例:
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testRBucketExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient client = redissonManager.getRedisson();
// RList 继承了 java.util.List 接口
RBucket<String> rstring = client.getBucket("redission:test:bucket:string");
rstring.set("this is a string");
RBucket<UserDTO> ruser = client.getBucket("redission:test:bucket:user");
UserDTO dto = new UserDTO();
dto.setToken(UUID.randomUUID().toString());
ruser.set(dto);
System.out.println("string is: " + rstring.get());
System.out.println("dto is: " + ruser.get());
client.shutdown();
}
}
运行上面的代码时,可以获得以下输出:
string is: this is a string
dto is: UserDTO(id=null, userId=null, username=null, password=null, nickname=null,
token=183b6eeb-65a8-4b2a-80c6-cf17c08332ce, createTime=null, updateTime=null, headImgUrl=null,
mobile=null, sex=null, enabled=null, type=null, openId=null, isDel=false)
3.2 使用RList
操作Redis
列表
下面的代码简单演示了如何在Redisson
中使用RList
对象。RList
是Java
的List
集合的分布式并发实现。
考虑以下代码:
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testListExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient client = redissonManager.getRedisson();
// RList 继承了 java.util.List 接口
RList<String> nameList = client.getList("redission:test:nameList");
nameList.clear();
nameList.add("张三");
nameList.add("李四");
nameList.add("王五");
nameList.remove(-1);
System.out.println("List size: " + nameList.size());
boolean contains = nameList.contains("李四");
System.out.println("Is list contains name '李四': " + contains);
nameList.forEach(System.out::println);
client.shutdown();
}
}
运行上面的代码时,可以获得以下输出:
List size: 2
Is list contains name '李四': true
张三
李四
3.3 使用RMap操作Redis哈希
Redisson
还包括RMap
,它是Java Map
集合的分布式并发实现,考虑以下代码:
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testListExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient client = redissonManager.getRedisson();
// RMap 继承了 java.util.concurrent.ConcurrentMap 接口
RMap<String, Object> map = client.getMap("redission:test:personalMap");
map.put("name", "张三");
map.put("address", "北京");
map.put("age", new Integer(50));
System.out.println("Map size: " + map.size());
boolean contains = map.containsKey("age");
System.out.println("Is map contains key 'age': " + contains);
String value = String.valueOf(map.get("name"));
System.out.println("Value mapped by key 'name': " + value);
client.shutdown();
}
}
运行上面的代码时,将会看到以下输出:
Map size: 3
Is map contains key 'age': true
Value mapped by key 'name': 张三
3.4 使用RLock实现Redis分布式锁
RLock
是Java
中可重入锁的分布式实现,下面的代码演示了RLock
的用法:
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testLockExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient redisson = redissonManager.getRedisson();
// RLock 继承了 java.util.concurrent.locks.Lock 接口
RLock lock = redisson.getLock("redission:test:lock:1");
final int[] count = {0};
int threads = 10;
ExecutorService pool = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(threads);
long start = System.currentTimeMillis();
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
for (int j = 0; j < 1000; j++) {
lock.lock();
count[0]++;
lock.unlock();
}
countDownLatch.countDown();
});
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("10个线程每个累加1000为: = " + count[0]);
//输出统计结果
float time = System.currentTimeMillis() - start;
System.out.println("运行的时长为:" + time);
System.out.println("每一次执行的时长为:" + time / count[0]);
}
}
此代码将产生以下输出:
10个线程每个累加1000为:= 10000
运行的时长为:14172.0
每一次执行的时长为:1.4172
3.5 使用RAtomicLong实现Redis原子操作
RAtomicLong
是Java
中AtomicLong
类的分布式“替代品”,用于在并发环境中保存长值。以下示例代码演示了RAtomicLong
的用法:
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testRAtomicLongExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient redisson = redissonManager.getRedisson();
RAtomicLong atomicLong = redisson.getAtomicLong("redission:test:myLong");
// 线程数
final int threads = 10;
// 每条线程的执行轮数
final int turns = 1000;
ExecutorService pool = Executors.newFixedThreadPool(threads);
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
try {
for (int j = 0; j < turns; j++) {
atomicLong.incrementAndGet();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
ThreadUtil.sleepSeconds(5);
System.out.println("atomicLong: " + atomicLong.get());
redisson.shutdown();
}
}
此代码的输出将是:
atomicLong: 10000
3.6 使用RLongAdder实现Redis原子操作
基于Redis
的Redisson
分布式长整型累加器(LongAdder)采用了与java.util.concurrent.atomic.LongAdder
类似的接口。通过利用客户端内置的LongAdder
对象,为分布式环境下递增和递减操作提供了很高得性能。据统计其性能最高比分布式AtomicLong
对象快12000倍。完美适用于分布式统计计量场景。下面是RLongAdder
的使用案例:
RLongAdder atomicLong = redisson.getLongAdder("myLongAdder");
atomicLong.add(12);
atomicLong.increment();
atomicLong.decrement();
atomicLong.sum();
以下示例代码演示了RLongAdder
的用法:
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testRAtomicLongExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient redisson = redissonManager.getRedisson();
RLongAdder atomicLong = redisson.getLongAdder("redission:test:myLongAdder");
// 线程数
final int threads = 10;
// 每条线程的执行轮数
final int turns = 1000;
ExecutorService pool = Executors.newFixedThreadPool(threads);
for (int i = 0; i < threads; i++) {
pool.submit(() -> {
try {
for (int j = 0; j < turns; j++) {
atomicLong.increment();
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
ThreadUtil.sleepSeconds(5);
System.out.println("atomicLong: " + atomicLong.get());
atomicLong.destroy();
redisson.shutdown();
}
}
此代码将产生以下输出:
longAdder:10000
运行的时长为:5085.0
每一次执行的时长为:0.5085
当不再使用长整型累加器对象的时候应该自行手动销毁,如果Redisson
对象被关闭(shutdown)了,则不用手动销毁。
RLongAdder atomicLong = ...
atomicLong.destroy();
3.7 序列化
Redisson
的对象编码类是用于将对象进行序列化和反序列化,以实现对该对象在Redis
里的读取和存储。Redisson
提供了对象编码应用请参考数据序列化。由Redisson
默认的编码器为二进制编码器,为了序列化后的内容可见,需要使用Json
文本序列化编码工具类。Redisson
提供了编码器JsonJacksonCodec
,作为Json
文本序列化编码工具类。
问题是:
JsonJackson
在序列化有双向引用的对象时,会出现无限循环异常。而fastjson在检查出双向引用后会自动用引用符$ref替换,终止循环。
所以,一些特殊场景中:用fastjson
能正常序列化到redis
,而JsonJackson
则抛出无限循环异常。为了序列化后的内容可见,所以不用redission
其他自带的,自行实现fastjson
编码器:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufInputStream;
import io.netty.buffer.ByteBufOutputStream;
import org.redisson.client.codec.BaseCodec;
import org.redisson.client.protocol.Decoder;
import org.redisson.client.protocol.Encoder;
import java.io.IOException;
public class FastjsonCodec extends BaseCodec {
private final Encoder encoder = in -> {
ByteBuf out = ByteBufAllocator.DEFAULT.buffer();
try {
ByteBufOutputStream os = new ByteBufOutputStream(out);
JSON.writeJSONString(os, in, SerializerFeature.WriteClassName);
return os.buffer();
} catch (IOException e) {
out.release();
throw e;
} catch (Exception e) {
out.release();
throw new IOException(e);
}
};
private final Decoder<Object> decoder = (buf, state) ->
JSON.parseObject(new ByteBufInputStream(buf), Object.class);
@Override
public Decoder<Object> getValueDecoder() {
return decoder;
}
@Override
public Encoder getValueEncoder() {
return encoder;
}
}
替换的方法如下:
@Slf4j
public class StandaloneConfigImpl implements RedissonConfigService {
@Override
public Config createRedissonConfig(RedissonConfig redissonConfig) {
Config config = new Config();
try {
String address = redissonConfig.getAddress();
String password = redissonConfig.getPassword();
int database = redissonConfig.getDatabase();
String redisAddr = GlobalConstant.REDIS_CONNECTION_PREFIX.getConstant_value() + address;
config.useSingleServer().setAddress(redisAddr);
config.useSingleServer().setDatabase(database);
//密码可以为空
if (!StringUtils.isEmpty(password)) {
config.useSingleServer().setPassword(password);
}
log.info("初始化[单机部署]方式Config,redisAddress:" + address);
//config.setCodec( new FstCodec());
config.setCodec( new FastjsonCodec());
} catch (Exception e) {
log.error("单机部署 Redisson init error", e);
}
return config;
}
}
3.8 执行Lua脚本
Lua
是一种开源、简单易学、轻量小巧的脚本语言,用标准C
语言编写。
其设计的目的就是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
Redis
从2.6版本开始支持Lua
脚本,Redis
使用Lua
可以:
- 原子操作。
Redis
会将整个脚本作为一个整体执行,不会被中断。可以用来批量更新、批量插入 - 减少网络开销。多个
Redis
操作合并为一个脚本,减少网络时延 - 代码复用。客户端发送的脚本可以存储在
Redis
中,其他客户端可以根据脚本的id
调用。
public class RedissionTest {
@Resource
RedissonManager redissonManager;
@Test
public void testLuaExamples() {
// 默认连接上 127.0.0.1:6379
RedissonClient redisson = redissonManager.getRedisson();
redisson.getBucket("redission:test:foo").set("bar");
String r = redisson.getScript().eval(RScript.Mode.READ_ONLY,
"return redis.call('get', 'redission:test:foo')", RScript.ReturnType.VALUE);
System.out.println("foo: " + r);
// 通过预存的脚本进行同样的操作
RScript s = redisson.getScript();
// 首先将脚本加载到Redis
String sha1 = s.scriptLoad("return redis.call('get', 'redission:test:foo')");
// 返回值 res == 282297a0228f48cd3fc6a55de6316f31422f5d17
System.out.println("sha1: " + sha1);
// 再通过SHA值调用脚本
Future<Object> r1 = redisson.getScript().evalShaAsync(RScript.Mode.READ_ONLY,
sha1, RScript.ReturnType.VALUE, ollections.emptyList());
try {
System.out.println("res: " + r1.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
client.shutdown();
}
}
运行上面的代码时,将会看到以下输出:
foo: bar
sha1: 282297a0228f48cd3fc6a55de6316f31422f5d17
res: bar
四、Redision锁的原理
Redis
发展到现在,几种常见的部署架构有:
- 单机模式;
- 哨兵模式;
- 集群模式;
先介绍,基于单机模式的Redision
锁的使用。
4.1 Redision锁的使用
单机模式下,Redision
锁的使用如下:
// 构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://172.29.1.180:5379").setPassword("a123456").setDatabase(0);
// 构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 设置锁定资源名称
RLock disLock = redissonClient.getLock("DISLOCK");
//尝试获取分布式锁
boolean isLock = disLock.tryLock(500, 15000, TimeUnit.MILLISECONDS);
if(isLock){
try {
//TODO if get lock success, do something;
Thread.sleep(15000);
} catch (Exception e) {
} finally {
// 无论如何, 最后都要解锁
disLock.unlock();
}
}
通过代码可知,经过
Redisson
的封装,实现Redis
分布式锁非常方便,和显式锁的使用方法是一样的。RLock
接口继承了Lock
接口。
我们再看一下Redis
中的value
是啥,和前文分析一样,hash
结构,redis
的key
就是资源名称。hash
结构的key
就是UUID+threadId,hash
结构的value
就是重入值,在分布式锁时,这个值为1(Redisson
还可以实现重入锁,那么这个值就取决于重入次数了):
172.29.1.180:5379> hgetall DISLOCK
1) "01a6d806-d282-4715-9bec-f51b9aa98110:1"
2) "1"
4.1.1 getLock()方法
@Override
public RLock getLock(String name){
return new RedissonLock(connectionManager.getCommandExecutor(), name);
}
可以看到,调用getLock()
方法后实际返回一个RedissonLock
对象
4.1.2 tryLock()方法
下面来看下tryLock
方法,源码如下:
@Override
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit)
throws InterruptedException {
long time = unit.toMillis(waitTime);
long current = System.currentTimeMillis();
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
current = System.currentTimeMillis();
RFuture<RedissonLockEntry> subscribeFuture = subscribe(threadId);
if (!subscribeFuture.await(time, TimeUnit.MILLISECONDS)) {
if (!subscribeFuture.cancel(false)) {
subscribeFuture.onComplete((res, e) -> {
if (e == null) {
unsubscribe(subscribeFuture, threadId);
}
});
}
acquireFailed(threadId);
return false;
}
try {
time -= System.currentTimeMillis() - current;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
while (true) {
long currentTime = System.currentTimeMillis();
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return true;
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
// waiting for message
currentTime = System.currentTimeMillis();
if (ttl >= 0 && ttl < time) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().tryAcquire(time, TimeUnit.MILLISECONDS);
}
time -= System.currentTimeMillis() - currentTime;
if (time <= 0) {
acquireFailed(threadId);
return false;
}
}
} finally {
unsubscribe(subscribeFuture, threadId);
}
// return get(tryLockAsync(waitTime, leaseTime, unit));
}
以上代码使用了异步回调模式,RFuture
继承了java.util.concurrent.Future
,CompletionStage
两大接口,异步回调模式的基础知识
4.1.3 tryAcquire()方法
在RedissonLock
对象的lock()
方法主要调用tryAcquire()
方法
@Override
public void lockInterruptibly() throws InterruptedException {
lock(-1, null, true);
}
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
lock(leaseTime, unit, true);
}
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
RFuture<RedissonLockEntry> future = subscribe(threadId);
if (interruptibly) {
commandExecutor.syncSubscriptionInterrupted(future);
} else {
commandExecutor.syncSubscription(future);
}
try {
while (true) {
ttl = tryAcquire(-1, leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
try {
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
if (interruptibly) {
throw e;
}
future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
}
} else {
if (interruptibly) {
future.getNow().getLatch().acquire();
} else {
future.getNow().getLatch().acquireUninterruptibly();
}
}
}
} finally {
unsubscribe(future, threadId);
}
//get(lockAsync(leaseTime, unit));
}
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
4.1.4 tryLockInnerAsync()方法
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime,
TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId,
RedisCommands.EVAL_LONG);
} else {
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
由于leaseTime == -1
,于是走tryLockInnerAsync()
方法,这个方法才是关键
<T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId,
RedisStrictCommand<T> command) {
return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
}
首先,看一下evalWriteAsync
方法的定义
<T, R> RFuture<R> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType,
String script, List<Object> keys, Object ... params);
这和前面的jedis
调用lua
脚本类似,最后两个参数分别是keys
和params
。单独将调用的那一段摘出来看,实际调用是这样的:
commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
"if (redis.call('exists', KEYS[1]) == 0) then " +
"redis.call('hset', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return nil; " +
"end; " +
"return redis.call('pttl', KEYS[1]);",
Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
结合上面的参数声明,我们可以知道,这里KEYS[1]
就是getName()
,ARGV[2]
是getLockName(threadId)
假设:
- 前面获取锁时传的
name
是“DISLOCK”, - 假设调用的线程
ID
是1, - 假设成员变量
UUID
类型的id
是01a6d806-d282-4715-9bec-f51b9aa98110
那么KEYS[1]=DISLOCK
,ARGV[2]=01a6d806-d282-4715-9bec-f51b9aa98110:1
因此,这段脚本的意思是:
- 判断有没有一个叫“DISLOCK”的
key
- 如果没有,则在其下设置一个字段为“01a6d806-d282-4715-9bec-f51b9aa98110:1”,值为“1”的键值对,并设置它的过期时间
- 如果存在,则进一步判断“01a6d806-d282-4715-9bec-f51b9aa98110:1”是否存在,若存在,则其值加1,并重新设置过期时间
- 返回“DISLOCK”的生存时间(毫秒)
4.2 原理:redisson加锁机制
这里用的数据结构是hash
,hash
的结构是:key 字段1 值1 字段2 值2 。。。
用在锁这个场景下,key
就表示锁的名称,也可以理解为临界资源,字段就表示当前获得锁的线程。所有竞争这把锁的线程都要判断在这个key
下有没有自己线程的字段,如果没有则不能获得锁,如果有,则相当于重入,字段值加1(次数)
4.2.1 Lua脚本的详解
为何要使用lua
语言?
因为一大堆复杂的业务逻辑,可以通过封装在lua
脚本中发送给redis
,保证这段复杂业务逻辑执行的原子性
回顾一下evalWriteAsync
方法的定义
<T, R> RFuture<R> evalWriteAsync(String key, Codec codec, RedisCommand<T> evalCommandType,
String script, List<Object> keys, Object ... params);
注意,其最后两个参数分别是keys
和params
。
4.2.2 关于lua脚本的参数解释:
KEYS[1]代表的是你加锁的那个key,比如说:
RLock lock = redisson.getLock("DISLOCK");
这里你自己设置了加锁的那个锁key
就是“DISLOCK”。
ARGV[1]代表的就是锁key的默认生存时间
调用的时候,传递的参数为
internalLockLeaseTime
,该值默认30秒。
ARGV[2]代表的是加锁的客户端的ID
,类似于下面这样:
01a6d806-d282-4715-9bec-f51b9aa98110:1
lua
脚本的第一段if
判断语句,就是用“exists DISLOCK”命令判断一下,如果你要加锁的那个锁key
不存在的话,你就进行加锁。
如何加锁呢?很简单,用下面的redis
命令:
hset DISLOCK 01a6d806-d282-4715-9bec-f51b9aa98110:1 1
通过这个命令设置一个hash
数据结构,这行命令执行后,会出现一个类似下面的数据结构:
DISLOCK:
{
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
}
接着会执行“pexpire DISLOCK 30000”命令,设置DISLOCK这个锁key的生存时间是30秒(默认)
4.2.3 锁互斥机制
那么在这个时候,如果客户端2来尝试加锁,执行了同样的一段lua脚本,会咋样呢?
很简单,第一个if
判断会执行“exists DISLOCK”,发现DISLOCK
这个锁key
已经存在了。接着第二个if
判断,判断一下,DISLOCK
锁key
的hash
数据结构中,是否包含客户端2的ID
,但是明显不是的,因为那里包含的是客户端1的ID
。所以,客户端2会获取到pttl DISLOCK返回的一个数字,这个数字代表了DISLOCK
这个锁key
的剩余生存时间。比如还剩15000毫秒的生存时间。此时客户端2会进入一个while
循环,不停的尝试加锁。
4.2.4 可重入加锁机制
如果客户端1都已经持有了这把锁了,结果可重入的加锁会怎么样呢?
RLock lock = redisson.getLock("DISLOCK")
lock.lock();
//业务代码
lock.lock();
//业务代码
lock.unlock();
lock.unlock();
分析上面那段lua
脚本。第一个if
判断肯定不成立,“exists DISLOCK”会显示锁key
已经存在了。第二个if
判断会成立,因为DISLOCK
的hash
数据结构中包含的那个ID
,就是客户端1的那个ID
,也就是“8743c9c0-0795-4907-87fd-6c719a6b4586:1”
此时就会执行可重入加锁的逻辑,他会用:
incrby DISLOCK
8743c9c0-0795-4907-87fd-6c719a6b4586:1 1
通过这个命令,对客户端1的加锁次数,累加1。此时DISLOCK
数据结构变为下面这样:
DISLOCK:
{
8743c9c0-0795-4907-87fd-6c719a6b4586:1 2
}
4.2.5 释放锁机制
如果执行lock.unlock()
,就可以释放分布式锁,此时的业务逻辑也是非常简单的。其实说白了,就是每次都对DISLOCK
数据结构中的那个加锁次数减1。如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:“del DISLOCK”命令,从redis
里删除这个key
。然后呢,另外的客户端2就可以尝试完成加锁了。
unlock源码
@Override
public void unlock() {
try {
get(unlockAsync(Thread.currentThread().getId()));
} catch (RedisException e) {
if (e.getCause() instanceof IllegalMonitorStateException) {
throw (IllegalMonitorStateException) e.getCause();
} else {
throw e;
}
}
}
再深入一下,实际调用的是unlockInnerAsync
方法
unlockInnerAsync()方法
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE,
internalLockLeaseTime, getLockName(threadId));
}
4.3 原理:Redision解锁机制
上面unlockInnerAsync
方法分析:
- 假设
name=DISLOCK
,线程ID是1。 KEYS[1]
是getName()
,即KEYS[1]=DISLOCK
KEYS[2]
是getChannelName()
,即KEYS[2]=redisson_lock__channel:{DISLOCK}
ARGV[1]
是LockPubSub.unlockMessage
,即ARGV[1]=0
ARGV[2]
是生存时间ARGV[3]
是getLockName(threadId)
,即ARGV[3]=8743c9c0-0795-4907-87fd-6c719a6b4586:1
因此,上面脚本的意思是:
1、判断是否存在一个叫“DISLOCK”的key
2、如果不存在,返回nil
3、如果存在,使用Redis
Hincrby命令用于为哈希表中的字段值加上指定增量值 -1 ,代表减去1
4、若counter
>,返回空,若字段存在,则字段值减1
5、若减完以后,counter
> 0 值仍大于0,则返回0
6、减完后,若字段值小于或等于0,则用publish
命令广播一条消息,广播内容是0,并返回1;
可以猜测,广播0表示资源可用,即通知那些等待获取锁的线程现在可以获得锁了
4.3.1 通过redis Channel解锁订阅
以上是正常情况下获取到锁的情况,那么当无法立即获取到锁的时候怎么办呢?
再回到前面获取锁的位置
@Override
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
return;
}
// 订阅
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
try {
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
// lock acquired
if (ttl == null) {
break;
}
// waiting for message
if (ttl >= 0) {
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
getEntry(threadId).getLatch().acquire();
}
}
} finally {
unsubscribe(future, threadId);
}
//get(lockAsync(leaseTime, unit));
}
protected static final LockPubSub PUBSUB = new LockPubSub();
protected RFuture<RedissonLockEntry> subscribe(long threadId) {
return PUBSUB.subscribe(getEntryName(), getChannelName(),
commandExecutor.getConnectionManager().getSubscribeService());
}
protected void unsubscribe(RFuture<RedissonLockEntry> future, long threadId) {
PUBSUB.unsubscribe(future.getNow(), getEntryName(), getChannelName(),
commandExecutor.getConnectionManager().getSubscribeService());
}
这里会订阅Channel
,当资源可用时可以及时知道,并抢占,防止无效的轮询而浪费资源
这里的channel
为:
redisson_lock__channel:
值为:
getEntryName()即是faa78ba3-9579-406c-874f-fe00202b9ebs:DISLOCK
getChannelName()即是redisson_lock__channel:
当资源可用用的时候,循环去尝试获取锁,由于多个线程同时去竞争资源,所以这里用了信号量,对于同一个资源只允许一个线程获得锁,其它的线程阻塞
4.3.2 watch dog自动延期机制
客户端1加锁的锁key
默认生存时间才30秒,如果超过了30秒,客户端1还想一直持有这把锁,怎么办呢?
简单!只要客户端1一旦加锁成功,就会启动一个watch dog
看门狗,他是一个后台线程,会每隔10秒检查一下,如果客户端1还持有锁key
,那么就会不断的延长锁key
的生存时间。
4.4 使用watchDog机制实现锁的续期
但是聪明的同学肯定会问:
有效时间设置多长,假如我的业务操作比有效时间长,我的业务代码还没执行完,就自动给我解锁了,不就完蛋了吗。
这个问题就有点棘手了,在网上也有很多讨论:第一种解决方法就是靠程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。但是这并不是万全之策,比如网络抖动这种情况是无法预测的,也有可能导致业务代码执行的时间变长,所以并不安全。第二种方法,使用监事狗watchDog
机制实现锁的续期。
第二种方法比较靠谱一点,而且无业务入侵。
在Redisson
框架实现分布式锁的思路,就使用watchDog
机制实现锁的续期。当加锁成功后,同时开启守护线程,默认有效期是30秒,每隔10秒就会给锁续期到30秒,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。
这里,和前面解决
JVM STW
的锁过期问题有点类似,只不过,watchDog
自动续期,也没有完全解决JVM STW
的锁过期问题。
4.4.1 redisson watchdog使用和原理
实际上,redisson
加锁的基本流程图如下:
这里专注于介绍watchdog
。首先watchdog
的具体思路是:加锁时,默认加锁30秒,每10秒钟检查一次,如果存在就重新设置过期时间为30秒。然后设置默认加锁时间的参数是lockWatchdogTimeout
(监控锁的看门狗超时,单位:毫秒),官方文档描述如下:
lockWatchdogTimeout(监控锁的看门狗超时,单位:毫秒)默认值:
30000
监控锁的看门狗超时时间单位为毫秒。该参数只适用于分布式锁的加锁请求中未明确使用leaseTimeout
参数的情况。如果该看门狗未使用lockWatchdogTimeout
去重新调整一个分布式锁的lockWatchdogTimeout
超时,那么这个锁将变为失效状态。这个参数可以用来避免由Redisson客户端节点宕机或其他原因造成死锁的情况。
需要注意的是:
watchDog
只有在未显示指定加锁时间时才会生效。(这点很重要)lockWatchdogTimeout
设定的时间不要太小,比如我之前设置的是100毫秒,由于网络直接导致加锁完后,watchdog
去延期时,这个key
在redis
中已经被删除了。
4.4.2 tryAcquireAsync原理
在调用lock
方法时,会最终调用到tryAcquireAsync
。详细解释如下:
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime,
TimeUnit unit, long threadId) {
//如果指定了加锁时间,会直接去加锁
if (leaseTime != -1) {
return tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
}
//没有指定加锁时间 会先进行加锁,并且默认时间就是 LockWatchdogTimeout的时间
//这个是异步操作 返回RFuture 类似netty中的future
RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(waitTime,
commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(),
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
//这里也是类似netty Future 的addListener,在future内容执行完成后执行
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// lock acquired
if (ttlRemaining == null) {
//这里是定时执行 当前锁自动延期的动作
scheduleExpirationRenewal(threadId);
}
});
return ttlRemainingFuture;
}
scheduleExpirationRenewal
中会调用renewExpiration
。
4.4.3 renewExpiration执行延期动作
这里我们可以看到是启用了一个timeout
定时,去执行延期动作
private void renewExpiration() {
Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) throws Exception {
ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
if (ent == null) {
return;
}
Long threadId = ent.getFirstThreadId();
if (threadId == null) {
return;
}
RFuture<Boolean> future = renewExpirationAsync(threadId);
future.onComplete((res, e) -> {
if (e != null) {
log.error("Can't update lock " + getName() + " expiration", e);
return;
}
if (res) {
//如果 没有报错,就再次定时延期
// reschedule itself
renewExpiration();
}
});
}
// 这里我们可以看到定时任务 是 lockWatchdogTimeout 的1/3时间去执行 renewExpirationAsync
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
最终scheduleExpirationRenewal
会调用到renewExpirationAsync
,
4.4.4 renewExpirationAsync
执行下面这段lua
脚本。他主要判断就是这个锁是否在redis
中存在,如果存在就进行pexpire
延期。
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
"redis.call('pexpire', KEYS[1], ARGV[1]); " +
"return 1; " +
"end; " +
"return 0;",
Collections.singletonList(getName()),
internalLockLeaseTime, getLockName(threadId));
}
4.4.5 watchLog总结
- 要使
watchLog
机制生效,lock
时不要设置过期时间 watchlog
的延时时间 可以由lockWatchdogTimeout
指定默认延时时间,但是不要设置太小。如100watchdog
会每lockWatchdogTimeout/3
时间,去延时。watchdog
通过类似netty
的Future
功能来实现异步延时watchdog
最终还是通过lua
脚本来进行延时
五、Redisson框架的分布式锁
Redisson
框架十分强大,除了前面介绍的getLock
方法获取的分布式锁(输入可重入锁的类型),还有很多其他的分布式锁类型。
5.1 分布式锁和同步器
总体的Redisson
框架的分布式锁类型,大致如下:
- Reentrant Lock(可重入锁)
- Fair Lock(公平锁)
- MultiLock(联锁)
- RedLock(红锁)
- ReadWriteLock(读写锁)
- Semaphore(信号量)
- PermitExpirableSemaphore(可过期性信号量)
- CountDownLatch(闭锁/倒数闩)
5.1.1 Reentrant Lock(可重入锁)
Redisson
的分布式可重入锁RLock
Java对象实现了java.util.concurrent.locks.Lock
接口,同时还支持自动过期解锁。
public void testReentrantLock(RedissonClient redisson) {
RLock lock = redisson.getLock("anyLock");
try {
// 1. 最常见的使用方法
// lock.lock();
// 2. 支持过期解锁功能,10秒钟以后自动解锁, 无需调用unlock方法手动解锁
// lock.lock(10, TimeUnit.SECONDS);
// 3. 尝试加锁,最多等待3秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (res) { //成功
// do your business
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
Redisson
同时还为分布式锁提供了异步执行的相关方法:
public void testAsyncReentrantLock(RedissonClient redisson) {
RLock lock = redisson.getLock("anyLock");
try {
lock.lockAsync();
lock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = lock.tryLockAsync(3, 10, TimeUnit.SECONDS);
if (res.get()) {
// do your business
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
5.1.2 Fair Lock(公平锁)
Redisson
分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock
接口的一种RLock
对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson
客户端线程同时请求加锁时,优先分配给先发出请求的线程。所有请求线程会在一个队列中排队,当某个线程出现宕机时,Redisson
会等待5秒后继续下一个线程,也就是说如果前面有5个线程都处于等待状态,那么后面的线程会等待至少25秒。
public void testFairLock(RedissonClient redisson) {
RLock fairLock = redisson.getFairLock("anyLock");
try {
// 最常见的使用方法
fairLock.lock();
// 支持过期解锁功能, 10秒钟以后自动解锁,无需调用unlock方法手动解锁
fairLock.lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
fairLock.unlock();
}
}
Redisson
同时还为分布式可重入公平锁提供了异步执行的相关方法:
RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);
5.1.3 MultiLock(联锁)
Redisson
分布式联锁RedissonMultiLock
对象可以将多个RLock
对象关联为一个联锁,每个RLock
对象实例可以来自于不同的Redisson
实例。
public void testMultiLock(RedissonClient redisson1, RedissonClient redisson2,
RedissonClient redisson3) {
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonMultiLock lock = new RedissonMultiLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1 lock2 lock3, 所有的锁都上锁成功才算成功。
lock.lock();
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
5.1.4 RedLock(红锁)
Redisson
红锁RedissonRedLock
对象实现了Redlock
介绍的加锁算法。该对象也可以用来将多个RLock
对象关联为一个红锁,每个RLock
对象实例可以来自于不同的Redisson
实例。
public void testRedLock(RedissonClient redisson1, RedissonClient redisson2,
RedissonClient redisson3) {
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
try {
// 同时加锁:lock1 lock2 lock3, 红锁在大部分节点上加锁成功就算成功。
lock.lock();
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
5.1.5 ReadWriteLock(读写锁)
Redisson
的分布式可重入读写锁RReadWriteLock
Java对象实现了java.util.concurrent.locks.ReadWriteLock
接口。同时还支持自动过期解锁。该对象允许同时有多个读取锁,但是最多只能有一个写入锁。
RReadWriteLock rwlock = redisson.getLock("anyRWLock");
// 最常见的使用方法
rwlock.readLock().lock();
// 或
rwlock.writeLock().lock();
// 支持过期解锁功能
// 10秒钟以后自动解锁
// 无需调用unlock方法手动解锁
rwlock.readLock().lock(10, TimeUnit.SECONDS);
// 或
rwlock.writeLock().lock(10, TimeUnit.SECONDS);
// 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
boolean res = rwlock.readLock().tryLock(100, 10, TimeUnit.SECONDS);
// 或
boolean res = rwlock.writeLock().tryLock(100, 10, TimeUnit.SECONDS);
// ...
lock.unlock();
5.1.6 Semaphore(信号量)
Redisson
的分布式信号量Semaphore
Java对象RSemaphore
采用了与java.util.concurrent.Semaphore
相似的接口和用法。
RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire();
//或
semaphore.acquireAsync();
semaphore.acquire(23);
semaphore.tryAcquire();
//或
semaphore.tryAcquireAsync();
semaphore.tryAcquire(23, TimeUnit.SECONDS);
//或
semaphore.tryAcquireAsync(23, TimeUnit.SECONDS);
semaphore.release(10);
semaphore.release();
//或
semaphore.releaseAsync();
5.1.7 PermitExpirableSemaphore(可过期性信号量)
Redisson
的可过期性信号量(PermitExpirableSemaphore),是在RSemaphore
对象的基础上,为每个信号增加了一个过期时间。每个信号可以通过独立的ID
来辨识,释放时只能通过提交这个ID
才能释放。
RPermitExpirableSemaphore semaphore = redisson.getPermitExpirableSemaphore("mySemaphore");
String permitId = semaphore.acquire();
// 获取一个信号,有效期只有2秒钟。
String permitId = semaphore.acquire(2, TimeUnit.SECONDS);
// ...
semaphore.release(permitId);
5.1.8 CountDownLatch(闭锁/倒数闩)
Redisson
的分布式闭锁(CountDownLatch),Java
对象RCountDownLatch
采用了与java.util.concurrent.CountDownLatch
相似的接口和用法。
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.trySetCount(1);
latch.await();
// 在其他线程或其他JVM里
RCountDownLatch latch = redisson.getCountDownLatch("anyCountDownLatch");
latch.countDown();
5.2 redis分布式锁的高可用
关于Redis
分布式锁的高可用问题,大致如下:
在master-slave
的集群架构中,就是如果你对某个redis master
实例,写入了DISLOCK
这种锁key
的value
,此时会异步复制给对应的master slave
实例。但是,这个过程中一旦发生redis master
宕机,主备切换,redis slave
变为了redis master
。而此时的主从复制没有彻底完成的话,接着就会导致,客户端2来尝试加锁的时候,在新的redis master
上完成了加锁,而客户端1也以为自己成功加了锁。此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致脏数据的产生。
所以这个是redis master-slave
架构的主从异步复制导致的redis
分布式锁的最大缺陷:
在
redis master
实例宕机的时候,可能导致多个客户端同时完成加锁。
5.2.1 高可用的RedLock(红锁)原理
RedLock
算法思想:
不能只在一个
redis
实例上创建锁,应该是在多个redis
实例上创建锁,n / 2 + 1
,必须在大多数redis
节点上都成功创建锁,才能算这个整体的RedLock
加锁成功,避免说仅仅在一个redis
实例上加锁而带来的问题。
这个场景是假设有一个redis cluster
,有5
个redis master
实例。然后执行如下步骤获取一把红锁:
- 获取当前时间戳,单位是毫秒;
- 跟上面类似,轮流尝试在每个
master
节点上创建锁,过期时间较短,一般就几十毫秒; - 尝试在大多数节点上建立一个锁,比如
5
个节点就要求是3
个节点n / 2 + 1
; - 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了;
- 要是锁建立失败了,那么就依次之前建立过的锁删除;
- 只要别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
RedLock
是基于redis
实现的分布式锁,它能够保证以下特性:
- 互斥性:在任何时候,只能有一个客户端能够持有锁;避免死锁:
- 当客户端拿到锁后,即使发生了网络分区或者客户端宕机,也不会发生死锁;(利用
key
的存活时间) - 容错性:只要多数节点的
redis
实例正常运行,就能够对外提供服务,加锁或者释放锁;
以sentinel
模式架构为例,如下图所示,有sentinel-1
,sentinel-2
,sentinel-3
总计3
个sentinel
模式集群,如果要获取分布式锁,那么需要向这3
个sentinel
集群通过EVAL
命令执行LUA脚本,需要3/2+1=2
,即至少2
个sentinel
集群响应成功,才算成功的以Redlock
算法获取到分布式锁:
高可用的红锁会导致性能降低
提前说明,使用redis
分布式锁,是追求高性能,在cap
理论中,追求的是ap
而不是cp
。所以,如果追求高可用,建议使用zookeeper
分布式锁。
redis分布式锁可能导致的数据不一致性,建议使用业务补偿的方式去弥补。所以,不太建议使用红锁,但是从学习的层面来说,大家还是一定要掌握的。
5.2.2 实现原理
Redisson
中有一个MultiLock
的概念,可以将多个锁合并为一个大锁,对一个大锁进行统一的申请加锁以及释放锁。而Redisson
中实现RedLock
就是基于MultiLock
去做的,接下来就具体看看对应的实现吧
RedLock使用案例
先看下官方的代码使用:(redlock)
RLock lock1 = redisson1.getLock("lock1");
RLock lock2 = redisson2.getLock("lock2");
RLock lock3 = redisson3.getLock("lock3");
RLock redLock = anyRedisson.getRedLock(lock1, lock2, lock3);
// traditional lock method
redLock.lock();
// or acquire lock and automatically unlock it after 10 seconds
redLock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
boolean res = redLock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
// ...
} finally {
redLock.unlock();
}
}
这里是分别对3个redis实例加锁,然后获取一个最后的加锁结果。
5.2.2.1 RedissonRedLock实现原理
上面示例中使用redLock.lock()
或者tryLock()
最终都是执行RedissonRedLock
中方法。RedissonRedLock
继承自RedissonMultiLock
,实现了其中的一些方法:
public class RedissonRedLock extends RedissonMultiLock {
public RedissonRedLock(RLock... locks) {
super(locks);
}
/**
* 锁可以失败的次数,锁的数量-锁成功客户端最小的数量
*/
@Override
protected int failedLocksLimit() {
return locks.size() - minLocksAmount(locks);
}
/**
* 锁的数量 / 2 + 1,例如有3个客户端加锁,那么最少需要2个客户端加锁成功
*/
protected int minLocksAmount(final List<RLock> locks) {
return locks.size()/2 + 1;
}
/**
* 计算多个客户端一起加锁的超时时间,每个客户端的等待时间
* remainTime默认为4.5s
*/
@Override
protected long calcLockWaitTime(long remainTime) {
return Math.max(remainTime / locks.size(), 1);
}
@Override
public void unlock() {
unlockInner(locks);
}
}
看到locks.size()/2 + 1
,例如我们有3个客户端实例,那么最少2个实例加锁成功才算分布式锁加锁成功。接着我们看下lock()
的具体实现
5.2.2.2 RedissonMultiLock实现原理
public class RedissonMultiLock implements Lock {
final List<RLock> locks = new ArrayList<RLock>();
public RedissonMultiLock(RLock... locks) {
if (locks.length == 0) {
throw new IllegalArgumentException("Lock objects are not defined");
}
this.locks.addAll(Arrays.asList(locks));
}
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
long newLeaseTime = -1;
if (leaseTime != -1) {
// 如果等待时间设置了,那么将等待时间 * 2
newLeaseTime = unit.toMillis(waitTime)*2;
}
// time为当前时间戳
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime = unit.toMillis(waitTime);
}
// 计算锁的等待时间,RedLock中:如果remainTime=-1,那么lockWaitTime为1
long lockWaitTime = calcLockWaitTime(remainTime);
// RedLock中failedLocksLimit即为n/2 + 1
int failedLocksLimit = failedLocksLimit();
List<RLock> acquiredLocks = new ArrayList<RLock>(locks.size());
// 循环每个redis客户端,去获取锁
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
// 调用tryLock方法去获取锁,如果获取锁成功,则lockAcquired=true
if (waitTime == -1 && leaseTime == -1) {
lockAcquired = lock.tryLock();
} else {
long awaitTime = Math.min(lockWaitTime, remainTime);
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
}
} catch (Exception e) {
lockAcquired = false;
}
// 如果获取锁成功,将锁加入到list集合中
if (lockAcquired) {
acquiredLocks.add(lock);
} else {
// 如果获取锁失败,判断失败次数是否等于失败的限制次数
// 比如,3个redis客户端,最多只能失败1次
// 这里locks.size = 3, 3-x=1,说明只要成功了2次就可以直接break掉循环
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
// 如果最大失败次数等于0
if (failedLocksLimit == 0) {
// 释放所有的锁,RedLock加锁失败
unlockInner(acquiredLocks);
if (waitTime == -1 && leaseTime == -1) {
return false;
}
failedLocksLimit = failedLocksLimit();
acquiredLocks.clear();
// 重置迭代器 重试再次获取锁
while (iterator.hasPrevious()) {
iterator.previous();
}
} else {
// 失败的限制次数减一
// 比如3个redis实例,最大的限制次数是1,如果遍历第一个redis实例,失败了,
// 那么failedLocksLimit会减成0
// 如果failedLocksLimit就会走上面的if逻辑,释放所有的锁,然后返回false
failedLocksLimit--;
}
}
if (remainTime != -1) {
remainTime -= (System.currentTimeMillis() - time);
time = System.currentTimeMillis();
if (remainTime <= 0) {
unlockInner(acquiredLocks);
return false;
}
}
}
if (leaseTime != -1) {
List<RFuture<Boolean>> futures = new ArrayList<RFuture<Boolean>>(acquiredLocks.size());
for (RLock rLock : acquiredLocks) {
RFuture<Boolean> future = rLock.expireAsync(unit.toMillis(leaseTime),
TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture : futures) {
rFuture.syncUninterruptibly();
}
}
return true;
}
}
核心代码都已经加了注释,实现原理其实很简单,基于RedLock
思想,遍历所有的Redis
客户端,然后依次加锁,最后统计成功的次数来判断是否加锁成功。
5.3 Redis分段锁
5.3.1 普通Redis分布式锁的性能瓶颈问题
分布式锁一旦加了之后,对同一个商品的下单请求,会导致所有下单操作,都必须对同一个商品key
加分布式锁。假设一个商品1分钟6000订单,每秒的600个下单操作,假设加锁之后,释放锁之前,查库存 -> 创建订单 -> 扣减库存,每个IO
操作100ms
,大概300毫秒。具体如下图:
可以再进行一下优化,将创建订单 + 扣减库存
并发执行,将两个100ms
减少为一个100ms,这既是空间换时间的思想,大概200毫秒。
将创建订单 + 扣减库存
批量执行,减少一次IO,也是大概200毫秒。
这个优化方案,有个重要的前提,就是订单表和库存表在相同的库中,但是,这个前提条件,在数据量大+高并发的场景下,够呛。
那么,一秒内,只能完成多少个商品的秒杀订单的下单操作呢?1000毫秒/200=5个订单。如何达到每秒600个下单呢?还是要从基础知识里边寻找答案?
5.3.2 分段加锁的思想来源
JUC
的LongAdder
和ConcurrentHashMap
的源码和底层原理,他们提升性能的办法是:
空间换时间,分段加锁
尤其是LongAdder
的实现思想,可以用于Redis
分布式锁作为性能提升的手段,将Redis
分布式锁优化为Redis
分段锁。
5.3.3 使用Redis分段锁提升秒杀的并发性能
回到前面的场景:
假设一个商品1分钟6000订单,每秒的600个下单操作,假设加锁之后,释放锁之前,查库存 -> 创建订单 -> 扣减库存,经过优化,每个IO操作100ms,大概200毫秒,一秒钟5个订单。
为了达到每秒600个订单,可以将锁分成600/5 =120个段,每一次使用随机算法,随机到一个分段,如果不行,就轮询下一个分段,具体的流程,大致如下:
缺点:
这个是一个理论的时间预估,没有扣除尝试下一个分段的时间,另外,实际上的性能,会比理论上差,从咱们实操案例的测试结果,也可以证明这点。