• 九、缓存击穿 + 聚划算demo


    一、基础知识

    1、概念

    大量的请求同时查询一个 key 时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。

    简单说就是热点key突然失效了,暴打mysql。

    2、危害

    会造成某一时刻数据库请求量过大,压力剧增。

    3、解决

    (1)互斥更新、随机退避、差异失效时间。

    互斥更新:缓存两套数据,访问的时候总会有一套存在,避免击穿问题

    随机退避:业务上规避

    差异失效时间:两套缓存失效时间不同

     

    (2)对于访问频繁的热点key,干脆就不设置过期时间

    (3)互斥独占锁防止击穿

    多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,然后做缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

    二、案例:淘宝聚划算功能实现+防止缓存击穿

    1、分析过程

    场景:

    1、高并发,用户访问量大。

    2、旧商品下线的同时,立马上线新商品。比如旧商品23:59:59下架,新商品00:00:00上架。

    3、每类商品只展示固定条数。

    步骤  说明
    1 100%高并发,绝对不可以用mysql实现
    2 先把mysql里面参加活动的数据抽取进redis,一般采用定时器扫描来决定上线活动还是下线取消。
    3 支持分页功能,一页20条记录
    Redis里面什么样子的数据类型支持上述功能? 答案:list

    2、entities

    @Data
    @ApiModel(value = "聚划算活动producet信息")
    public class Product {
        private Long id;
        /**
         * 产品名称
         */
        private String name;
        /**
         * 产品价格
         */
        private Integer price;
        /**
         * 产品详情
         */
        private String detail;
    
        public Product() {
        }
    
        public Product(Long id, String name, Integer price, String detail) {
            this.id = id;
            this.name = name;
            this.price = price;
            this.detail = detail;
        }
    }

    3、Service

    @Service
    @Slf4j
    public class JHSTaskService {
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 初始化数据
         */
        @PostConstruct
        public void initJHS() {
            log.info("启动定时器淘宝聚划算功能模拟.........." + DateUtil.now());
            new Thread(() -> {
                //模拟定时器,定时把数据库的特价商品,刷新到redis中
                while (true) {
                    //模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
                    List<Product> list = this.products();
    
                    //采用redis list数据结构的lpush来实现存储
                    //每次更新时,需要删除旧数据
                    this.redisTemplate.delete(Constants.JHS_KEY);
                    //lpush命令
                    this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY, list);
                    //间隔一分钟 执行一遍
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    log.info("runJhs定时刷新..............");
                }
            }, "t1").start();
        }
    
        /**
         * 模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
         */
        public List<Product> products() {
            List<Product> list = new ArrayList<>();
            for (int i = 1; i <= 20; i++) {
                Random rand = new Random();
                int id = rand.nextInt(10000);
                Product obj = new Product((long) id, "product" + i, i, "detail");
                list.add(obj);
            }
            return list;
        }
    }

    4、Controller

    @RestController
    @Slf4j
    @Api(tags = "聚划算商品列表接口")
    public class JHSProductController {
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 分页查询:在高并发的情况下,只能走redis查询,走db的话必定会把db打垮
         * http://localhost:5555/swagger-ui.html#/jhs-product-controller/findUsingGET
         */
        @RequestMapping(value = "/pruduct/find", method = RequestMethod.GET)
        @ApiOperation("按照分页和每页显示容量,点击查看")
        public List<Product> find(int page, int size) {
            List<Product> list = null;
            long start = (page - 1) * size;
            long end = start + size - 1;
            try {
                //采用redis list数据结构的lrange命令实现分页查询
                list = this.redisTemplate.opsForList().range(Constants.JHS_KEY, start, end);
                if (CollectionUtils.isEmpty(list)) {
                    //TODO 走DB查询
                }
                log.info("查询结果:{}", list);
            } catch (Exception ex) {
                //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
                log.error("exception:", ex);
                //TODO 走DB查询
            }
            return list;
        }
    }

    三、分析隐患和代码升级

    1、隐患

    2、升级Service

        @PostConstruct
        public void initJHSAB() {
            log.info("启动AB定时器计划任务淘宝聚划算功能模拟.........." + DateUtil.now());
            new Thread(() -> {
                //模拟定时器,定时把数据库的特价商品,刷新到redis中
                while (true) {
                    //模拟从数据库读取100件特价商品,用于加载到聚划算的页面中
                    List<Product> list = this.products();
                    //先更新B缓存
                    this.redisTemplate.delete(Constants.JHS_KEY_B);
                    this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_B, list);
                    this.redisTemplate.expire(Constants.JHS_KEY_B, 20L, TimeUnit.DAYS);
                    //再更新A缓存
                    this.redisTemplate.delete(Constants.JHS_KEY_A);
                    this.redisTemplate.opsForList().leftPushAll(Constants.JHS_KEY_A, list);
                    this.redisTemplate.expire(Constants.JHS_KEY_A, 15L, TimeUnit.DAYS);
                    //间隔一分钟 执行一遍
                    try {
                        TimeUnit.MINUTES.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
    
                    log.info("runJhs定时刷新..............");
                }
            }, "t1").start();
        }

    3、升级Controller

        @RequestMapping(value = "/pruduct/findab", method = RequestMethod.GET)
        @ApiOperation("按照分页和每页显示容量,点击查看AB")
        public List<Product> findAB(int page, int size) {
            List<Product> list = null;
            long start = (page - 1) * size;
            long end = start + size - 1;
            try {
                //采用redis list数据结构的lrange命令实现分页查询
                //  先查询缓存A,A失效了再查缓存B
                list = this.redisTemplate.opsForList().range(Constants.JHS_KEY_A, start, end);
                if (CollectionUtils.isEmpty(list)) {
                    log.info("=========A缓存已经失效了,记得人工修补,B缓存自动延续5天");
                    //用户先查询缓存A(上面的代码),如果缓存A查询不到(例如,更新缓存的时候删除了),再查询缓存B
                    this.redisTemplate.opsForList().range(Constants.JHS_KEY_B, start, end);
                }
                log.info("查询结果:{}", list);
            } catch (Exception ex) {
                //这里的异常,一般是redis瘫痪 ,或 redis网络timeout
                log.error("exception:", ex);
                //TODO 走DB查询
            }
            return list;
        }

    四、总结

  • 相关阅读:
    JDK所有版本
    application.yml配置log日志
    eclipse配置lombok
    Eclipse配置springboot
    java 连接mongodb
    MongoDB shell操作
    mysql插入一万条数据
    Web设计精髓(转)
    SyntaxHighlighter -- 代码高亮插件
    input之placeholder与行高的问题。
  • 原文地址:https://www.cnblogs.com/shiblog/p/15828544.html
Copyright © 2020-2023  润新知