Redis高并发下使用及注意事项

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();
        }
    }
}

数据库中表的数据


image.png

Redis缓存是空的:


image.png

准备测试:
没有加信号量的情况下:2000个请求都没有经过Redis缓存,直接去查询数据库了,耗时3.726s。


image.png

加了信号量的情况下:耗时2.816s。


image.png

image.png

结果:在高并发的情况下,使用信号量可以减少数据库的压力,提高访问性能。

查询数据击穿场景

在请求进来的时候,先对请求目标进行过滤,可以使用布隆过滤器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,有很多的模块


image.png

RedisBloom在github上的地址

https://github.com/RedisBloom/RedisBloom
image.png

你可能感兴趣的:(Redis高并发下使用及注意事项)