redisson操作redis的一款框架,用来处理分布式锁等功能的。支持Java语言
redisson
的github地址上面有详细的方法介绍==>官网地址
注明:如果是其他语言
的分布式锁,可以去看redis官网 ==> 官网地址
分布式情况下采用Redisson做分布式锁
如果是单体应用(就一个服务):我们完成可以采用本地锁(synchronized、lock)在进行同步。要区分好场景
本地锁 synchronized (this){} 同步代码块。只要是同一把锁,就能锁住需要这个锁的多线程
this==>springboot所有的组件在容器中都是单例的。所有注意这个this是唯一的
<!--引入redisson,作为所有分布式锁,分布式对象等功能的框架-->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
下面的案例会用到操作redis的所有也将redis依赖引入
<!-
这边是排除了lettuce,使用jedis做底层客户端
但是还是推荐是lettuce做底层客户端,他使用netty通信,吞吐量更大(不排除lettuce,就不用引入jedis)
lettuce有个bug。在压力测试的时候会产生堆外内存溢出,OutOfDirectMemoryError
但是据说已经修复了
jedis是稳定,但是太久没更新了
-->
<!--spring-boot-starter-data-redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!-- 排除掉lettuce客户端 -->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
小编采用程序化配置、单节点模式
MyRedissonConfig
package sqy.redissondome.config.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;
import java.io.IOException;
/**
* Redisson配置类
* @author suqinyi
* @Date 2022/1/14
*/
@Configuration
public class MyRedissonConfig {
/**
* 官网配置说明:
* https://github.com/redisson/redisson/wiki/2.-%E9%85%8D%E7%BD%AE%E6%96%B9%E6%B3%95
*
* Redisson来操作redis
* 所有对Redisson的使用都是通过RedissonClient对象
* 配置方法有
* 第一种、 程序化配置方法
* config.useSingleServer().setAddress("redis://127.0.0.1:6379");
* 第二种、 文件方式配置
* Config config = Config.fromYAML(new File("config-file.yaml"));
*
* 这是集群的程序化配置方法(方法1)
* .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
*/
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException {
//创建配置
Config config = new Config();
//redis的连接地址,这个是小编虚拟机的redis
//这边采用了【单节点模式】 - redis地址
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
//根据config创建出RedissonClient的实列
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
// //法一
// @Bean
// public RedissonClient getRedisson() {
// Config config = new Config();
// if(colony) {
// config.useClusterServers()
// .setScanInterval(1000 * 2)
// .addNodeAddress(addressUrl)
// .setPassword(password)
// .setReconnectionTimeout(10000)
// .setRetryInterval(5000)
// .setTimeout(10000)
// .setConnectTimeout(10000);
//
// }else {
// config.useSingleServer()
// .setAddress(addressUrl).setPassword(password)
// .setReconnectionTimeout(10000)
// .setRetryInterval(5000)
// .setTimeout(10000)
// .setConnectTimeout(10000);
// }
//
// return Redisson.create(config);
// //法二
// config.useMasterSlaveServers()
// //可以用"rediss://"来启用SSL连接
// .setMasterAddress(addressUrl).setPassword(password)
// .addSlaveAddress(addressUrlSlave)
// .setReconnectionTimeout(10000)
// .setRetryInterval(5000)
// .setTimeout(10000)
// .setConnectTimeout(10000);//(连接超时,单位:毫秒 默认值:3000);
// 哨兵模式config.useSentinelServers().setMasterName("mymaster").setPassword("web2017").addSentinelAddress("***(哨兵IP):26379", "***(哨兵IP):26379", "***(哨兵IP):26380");
}
在配置下redis
spring:
#配置redis
redis:
host: 192.168.56.10
port: 6379
3.2.1、配置方法==>入口
3.2.2、共有俩种配置方法
3.2.3、单节点模式
因为小编没有把redis搭集群,所有采用的是单节点模式
代码如下(示例):
可重入锁和读写锁
信号量(Semaphore)
:我们可以用来做限流闭锁(CountDownLatch)
也可以成为等待锁可重入锁
基于Redis的Redisson分布式可重入锁RLock
Java对象实现了java.util.concurrent.locks.Lock接口。
同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
所以 ==> 以前Lock是怎么使用的、JUC是怎么使用的,redisson就怎么使用
记得注入RedissonClient 和StringRedisTemplate
@Autowired
RedissonClient redisson;
@Autowired
StringRedisTemplate redisTemplate;
依赖包不要导入错了
import org.springframework.data.redis.core.StringRedisTemplate;
import org.redisson.api.*;
注:说明和源码的核心,小编写在注释里面了,可以自己搜索类进行看源码
@ResponseBody
@GetMapping("/hello")
public String hello() {
// 获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
//方式一、加锁,默认的
lock.lock();//阻塞式等待。默认加的锁都是30秒时间
//方式二、可以指定时间,锁到期以后没有自动续期 ==> 所以自动解锁的时间一定要保证大于业务的执行时间
//lock.lock(10,TimeUnit.SECONDS);//10s自动解锁
//源代码:RedissonLock ==> 利用 while(ture)
/**
* 看门狗机制
* 1、锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心因为业务时间长,锁自动过期被删除
* 2、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除
*
* 问题: lock.lock(10,TimeUnit.SECONDS) 在锁过期不会自动续期
* 1、如果我们传递了锁的过期时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间
* 2、如果没有指定锁的超时时间,就使用30000L(LockWatchdogTimeout 看门狗的默认时间)
* 只要占锁成功就会启动一个定时任务【就会重新给锁设置过期时间,新的时间就是看门狗的默认时间】,每隔10s都会自动续期,续期成30s
* this.internalLockLeaseTime(看门狗时间) / 3L 也就是10s续期一次
*/
/**
* 实战的使用推荐
* 推荐使用:lock.lock(10,TimeUnit.SECONDS) 省掉了整个续期操作。手动解锁
*/
try {
//业务
System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
//模拟业务耗时
Thread.sleep(30000);
} catch (Exception e) {
} finally {
/**
* 假设出现闪断,解锁代码没有运行,redisson会不会出现死锁?
* 不会出现死锁,锁的
*/
//解锁
System.out.println("释放锁..." + Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
测试:同时发送2个hello请求
说明:准备2个方法,一个读取数据,一个写入数据
读写锁的好处小编之间总结在代码注释上面
/**
* 读写锁的好处:
* 修改数据期间,写锁是一个排他锁(互斥锁\独享锁),读锁是一个共享锁
* 写锁没释放,读锁就必须等着
* 这样能保证读到最新数据
* 读 + 读 相当于无锁(并发读,只会在redis中记录好,所有当前的读锁,他们都会同时加锁成功)
* 写 + 读 等待写锁释放
* 写 + 写 阻塞方式
* 读 + 写 有读锁,写也需要等待
*
* 只要有写的存在,都必须等待
*/
@ResponseBody
@GetMapping("/write")
public String writeValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
/**
* 改数据加写锁,读数据加读锁
*/
//加 写锁
RLock rLock = lock.writeLock();
rLock.lock();
String s = "";
try {
System.out.println("写锁加锁成功。。。" + Thread.currentThread().getId());
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue", s);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("写锁释放成功。。。" + Thread.currentThread().getId());
//解锁
rLock.unlock();
}
return s;
}
@ResponseBody
@GetMapping("/read")
public String readValue() {
RReadWriteLock lock = redisson.getReadWriteLock("rw-lock");
//加 读锁
RLock rLock = lock.readLock();
rLock.lock();
String s = "";
try {
System.out.println("读锁加锁成功。。。" + Thread.currentThread().getId());
s = redisTemplate.opsForValue().get("writeValue");
/**
* 这边测试,先读在写
* 让读也延时30秒,看写锁是否需要等待
*/
Thread.sleep(30000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("读锁释放成功。。。" + Thread.currentThread().getId());
rLock.unlock();
}
return s;
}
测试:我们可以模拟并发读写
说明:你们可以将配置复制一份,修改名称和端口,开2个服务。这样来模拟2个服务器,同时,进行读写操作。效果是一样的
准备3个方法:一个在redis中存入数据,一个获取数据,一个释放数据
/**
* 信号量(Semaphore)
* 都是阻塞方法
* acquire() :会一直等,等到redis有数据,他就马上执行
* tryAcquire():进行判断,如果为false,直接返回
* 可以用于限流
* 车库停车 ==> 一共3位置
*/
//存入测试数据
@GetMapping("/setPark")
@ResponseBody
public String setPark() {
redisTemplate.opsForValue().set("park", "3");
return "一共有3个空闲";
}
//获取一个数据(acquire)
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire();//获取一个信息(获取一个值)占一个车位
//执行业务
String s = redisTemplate.opsForValue().get("park");
return "redis剩余:" + s;
}
//获取一个数据(trypark)
@GetMapping("/trypark")
@ResponseBody
public String trypark() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
boolean b = park.tryAcquire();
if (b) {
//执行业务
String s = redisTemplate.opsForValue().get("park");
return "redis剩余:" + s + " " + b;
} else {
return "使用tryAcquire限流。封顶了你被直接返回";
}
}
//释放一个数据
@GetMapping("/go")
@ResponseBody
public String go() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.release();//释放一个信息(释放一个值)释放一个车位
String s = redisTemplate.opsForValue().get("park");
return "redis剩余:" + s;
}
测试:
以上
,将存入redis的数据减少这种我们可以用来做限流操作
/**
* 放假,锁门
* 5个班级全部走完,锁大门
*/
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);//设置5个班
door.await();//等待闭锁都完成
return "放假了..";
}
//这个接口必须请求5次,闭锁才能释放
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();//计数减一。
return id+"班的人走完了。。。";
}
测试:
常见的2中模式:
这俩种模式:都可能因为网络问题,或者其他问题出现,在并发读写的情况下,产生脏数据(缓存和数据库最新数据不一致)
系统的一致性解决方案:
1、缓存的所有数据都有过期时间,数据过期下一次查询触发主动更新
2、读写数据的时候,加上分布式的读写锁。
经常写,经常读
有没有必要存入缓存,加锁也会影响效率
对经常读写的数据,要求实时性高的,直接去查数据库。
其他缓存当然还有:spring cache、guava…
推荐来看看官网的第三方框架整合(整合spring cache) ==> 入口
讲到分布式锁,就要有分布式事务。这边只能分篇章进行小结
其他的比如说:集群、分片…之间参考官方文档搞就行了
面向官方文档编程