Redis高并发下使用及注意事项
Redis 数据失效导致的雪崩
因为 缓存失效,从而导致大量请求 操作数据库:
1 .大量请求,导致数据库处理不过来,整个系统依赖数据库的功能全部 崩溃
2 .单系统挂掉,其他依赖于该系统的应用也会出现不稳定甚至 崩溃
Redis数据失效场景
1.数据淘汰LRU/LFU
2.数据过期expire
3.服务重启 ,宕机/升级
Redis雪崩解决方案
1.对数据库访问限流:信号量控制并发
2.容错降级:返回异常码
3.针对内存不足:采用redis集群方案
Redis缓存击穿场景
查询的数据不存在,请求透过redis,直接到数据库。
直接上代码
模拟一个商品在抢购活动的场景,即高并发情况下的情况:
IDEA创建SpringBoot项目:
1.pom依赖
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
org.apache.commons
commons-pool2
2.8.1
org.springframework.boot
spring-boot-starter-data-jdbc
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-devtools
runtime
true
org.projectlombok
lombok
true
junit
junit
4.13
test
org.springframework.boot
spring-boot-configuration-processor
test
2.application.yml配置文件
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
redis:
host: 127.0.0.1
port: 6379
database: 0
##在redis.conf配置文件里有requirepass是密码设置
password: 123456
ssl: false
timeout: 6000
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池中的最小空闲连接 默认 0
min-idle: 0
3.实体类Goods
@Data
public class Goods implements Serializable {
private int id;
private int quantity;
private String name;
private Double price;
}
数据库操作
@Component
public class DatabaseService {
@Autowired
JdbcTemplate jdbcTemplate;
public Goods queryFromDatabase(String goodsId){
String sql = "SELECT * FROM goods WHERE id = ?";
RowMapper rowMapper = new BeanPropertyRowMapper(Goods.class);
return jdbcTemplate.queryForObject(sql,new Object[]{goodsId},rowMapper);
}
}
业务处理
@Component
public class GoodsService {
private final Logger logger = LoggerFactory.getLogger(GoodsService.class);
@Autowired
RedisTemplate redisTemplate;
@Autowired
DatabaseService databaseService;
/**
* 单线程访问
* 高并发的时候,会同时对数据操作,有问题,需要改造
* @param goodsId
* @return
*/
public Goods queryData(String goodsId){
String cacheKey = "goods-"+goodsId;
Goods value = (Goods) redisTemplate.opsForValue().get(cacheKey);
if (value!=null){
logger.warn(Thread.currentThread().getName()+" 缓存中获取数据+++ goodsId="+goodsId+",value="+value.toString());
return value;
}
//查询数据库
value = databaseService.queryFromDatabase(goodsId);
System.err.println(Thread.currentThread().getName()+" 数据库中获取数据++++ goodsId="+goodsId+",value="+value.toString());
redisTemplate.opsForValue().set(cacheKey, value);
//key过期时间
// redisTemplate.opsForValue().set(cacheKey, value,50,TimeUnit.SECONDS);
return value;
}
//数据库限流 30,根据数据库设置
Semaphore semaphore = new Semaphore(30);
/**
* 信号量
* @param goodsId
* @return
*/
public Goods querySemaphore(String goodsId){
String cacheKey = "goods-"+goodsId;
Goods value = (Goods) redisTemplate.opsForValue().get(cacheKey);
if (value!=null){
logger.warn(Thread.currentThread().getName()+" 缓存中获取数据1+++ goodsId="+goodsId+",value="+value.toString());
return value;
}
//控制,限流
try {
//同一时间内,只有30个请求去数据库获取数据,并且重构缓存
//每30个一批次,等待5s钟,如果处理超时,则走else处理异常
boolean acquire = semaphore.tryAcquire(5, TimeUnit.SECONDS);
if (acquire){
value = (Goods) redisTemplate.opsForValue().get(cacheKey);
if (value!=null){
logger.warn(Thread.currentThread().getName()+" 缓存中获取数据2+++ goodsId="+goodsId+",value="+value.toString());
return value;
}
//
value = databaseService.queryFromDatabase(goodsId);
System.err.println(Thread.currentThread().getName()+" 数据中获取数据+++++++ goodsId="+goodsId+",value="+value.toString());
//把数据放入缓存
redisTemplate.opsForValue().set(cacheKey, value);
//key过期时间
// redisTemplate.opsForValue().set(cacheKey, value,50,TimeUnit.SECONDS);
}else {//异常提示
return null;
}
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
semaphore.release();
}
return value;
}
}
测试
造数据sql
public class createDateTests {
/**
* sql 生成数据
* @param args
*/
public static void main(String[] args) {
String[] goodsNames = new String[] {"宝马","比亚迪","apple",
"足球","吉利","联想","鼠标","红薯","橙子","水杯"};
Random random = new Random();
for(int i = 1; i < 500; i++) {
String goodsName = goodsNames[random.nextInt(goodsNames.length)]+"-"+i;
System.out.println("INSERT INTO goods (id, quantity, name, price)"
+" VALUES ("+i+", "+random.nextInt(2000)
+", '"+goodsName+"', "+(double)random.nextInt(1000)/10+");");
}
}
}
测试数据库连接
/**
* 测试数据连接
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = 启动类.class)
public class DBTests {
@Autowired
DatabaseService databaseService;
@Test
public void testDB(){
Goods data = databaseService.queryFromDatabase("100");
System.err.println("测试数据库是否连通:"+data);
}
}
模拟高并发请求商品
/**
*import org.junit.After;
* import org.junit.Before;
* import org.junit.Test;
* import org.junit.runner.RunWith;
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = 启动类.class)
public class LearnRedisSemaphoreApplicationTests {
long time = 0L;
@Before
public void start() {
System.out.println("开始测试");
time = System.currentTimeMillis();
}
@After
public void end() {
System.out.println("结束测试,执行时长:" + (System.currentTimeMillis() - time ));
}
// 商品
private static final String Goods_ID = "180";
// 模拟的请求数量
private static final int THREAD_NUM = 2000;
// 倒计数器 juc包中常用工具类
private CountDownLatch countDownLatch = new CountDownLatch(1);
@Autowired
GoodsService service;
@Test
public void benchData() throws InterruptedException {
// 创建 并不是马上发起请求
Thread[] threads = new Thread[THREAD_NUM];
Random random = new Random();
for (int i = 0; i < THREAD_NUM; i++) {
// 多线程模拟用户查询请求
Thread thread = new Thread(() -> {
try {
String gId = Goods_ID;
// 代码在这里等待,等待countDownLatch为0,代表所有线程都start,再运行后续的代码
countDownLatch.await();
// http请求,实际上就是多线程调用这个方法
service.queryData(gId);
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i] = thread;
thread.start();
}
// 启动后,倒计时器倒计数 减一,代表又有一个线程就绪了
countDownLatch.countDown();
// 等待上面所有线程执行完毕之后,结束测试
for (Thread thread : threads) {
thread.join();
}
}
/**
* 信号量
* @throws InterruptedException
*/
@Test
public void benchData_semaphore() throws InterruptedException {
// 创建 并不是马上发起请求
Thread[] threads = new Thread[THREAD_NUM];
Random random = new Random();
for (int i = 0; i < THREAD_NUM; i++) {
// 多线程模拟用户查询请求
Thread thread = new Thread(() -> {
try {
String gId = Goods_ID;
// 代码在这里等待,等待countDownLatch为0,代表所有线程都start,再运行后续的代码
countDownLatch.await();
// http请求,实际上就是多线程调用这个方法
service.querySemaphore(gId);
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i] = thread;
thread.start();
}
// 启动后,倒计时器倒计数 减一,代表又有一个线程就绪了
countDownLatch.countDown();
// 等待上面所有线程执行完毕之后,结束测试
for (Thread thread : threads) {
thread.join();
}
}
}
数据库中表的数据
Redis缓存是空的:
准备测试:
没有加信号量的情况下:2000个请求都没有经过Redis缓存,直接去查询数据库了,耗时3.726s。
加了信号量的情况下:耗时2.816s。
结果:在高并发的情况下,使用信号量可以减少数据库的压力,提高访问性能。
查询数据击穿场景
在请求进来的时候,先对请求目标进行过滤,可以使用布隆过滤器Bloom Filter。
布隆过滤器Bloom Filter
布隆过滤器Bloom Filter是1970年布隆提出。
是一个很长的二进制数组和一系列hash函数。
用于检索一个元素是否在一个集合中。
它的优点是空间效率和查询时间都比一般的算法要好很多。
缺点是有一定的误识别率和删除困难。
利用Redis特性和命令:bitmaps (SETBIT设置指定位置的值,GETBIT获取值),也就是Redis自带的二进制数组特性。
数组构建:加载符合条件的记录,并将每一条记录对应的bitmaps位置设为1
二进制数组构建过程
1.加载符合条件的记录
2.计算每条元素的hash值
3.计算hash值对应二进制数组的位置
4.将对应位置的值改为1
查找元素是否存在的过程:
1.计算元素的hash的值
2.计算hash值对应二进制数组的位置
3.找到数组中对应位置的值,0代表不存在,1代表存在
在Redis官网上,有一个Modules,有很多的模块
RedisBloom在github上的地址
https://github.com/RedisBloom/RedisBloom