推荐学习

  • 都是“Redis惹的祸”,害我差点挂在美团三面,真是“虚惊一场”
  • 一问Kafka就心慌?我却凭着这份《Kafka源码实战》碾压面试官!

缓存是互联网开发中必不可少的一部分,它能降低我们数据库的并发数,提高我们系统的性能,比如我们经常使用的redis、emCached等等,其中redis应该是大部分的人选,为什么?因为速度快,易上手,是很多开发者的首选,但是缓存同样存在这问题,如果使用的不恰当,也可能会造成非常严重的后果,这时候你可能就会有疑问,缓存只是存储一些数据而已,怎么会造成严重的后果呢?下面我就带大家一起来分析分析。

什么是缓存

缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一。比如我们的redis、他就是缓存中比较常见的一种,他的并发读写能力能达到10w/s左右的速度,这个速度是相当不错的,相对于传统的数据存储来说,比如数据库,快了不知道多少倍,传统的数据库(mysql)操作的都是磁盘,而redis操作的是内存(ram),所以他们的速度肯定是没法比较的,由于传统数据库的读写较慢,所以并发较高的时候就会造成性能瓶颈问题,这也是为什么需要引入缓存的原因之一。

人在地上走,锅从天上来

我是一个快乐的程序狗,每天最快乐的事情就是codding,最大的愿望就是能准时6点下班,然后回家,但是今天肯定是走不了了,现在是17:30,我们的测试小哥哥给我提了一个很诡异的bug,难受啊,我的准时下班梦,但是作为一个程序狗,肯定都有着一颗和bug战斗到底的决心,究竟是什么bug呢?bug是这样的:并发请求订单信息,没过几秒就抛出系统错误。这个bug看着没几个字,但是一看就知道不好解决,尤其是像这种并发bug,能让人瞬间白了头,随后我找到了测试,让他们复现了这个神秘的bug,而我也找到了产生这个bug的来源,并且快速的修复了他,到底是什么问题呢?是因为小明(同事)在编写代码的时候考虑的不是很周全导致的,所以开发一定要想仔细了再动手,否则吃亏的就是自己啊。

缓存穿透

什么是缓存穿透

缓存穿透指的是:同一时刻,大量的并发请求数据库中不存在的信息,他既不会命中缓存,也不会命中数据库,但是他会查找数据库。上面的bug也是因为它产生的,测试的小哥哥查询的订单都是数据库不存在的,所以这个时候这些并发请求都不会命中缓存(redis),将直达数据库(mysql),由于大量的并发请求到达数据库,而数据库承受不住这么高的并发,从而导致数据库奔溃,这就是缓存穿透。重现bug1.新建数据表:订单表,结构如下:

2.编写测试代码OrderBo.java

package com.ymy.bo;import lombok.Data;import java.io.Serializable;import java.math.BigDecimal;@Datapublic class OrderBo implements  Serializable {    /**     * 自增id     */    private Long id;    /**     *订单编号     */    private Long orderCode;    /**     *订单价格     */    private BigDecimal orderPrice;    /**     *商品名称     */    private String  peoductName;    /**     *创建时间     */    private String createTime;}

OrderController.java

package com.ymy.controller;import com.ymy.bo.OrderBo;import com.ymy.service.OrderService;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class OrderController {    private OrderService orderService;    public OrderController(OrderService orderService){        this.orderService = orderService;    }    @RequestMapping(value = "/detail",method = RequestMethod.GET)    public OrderBo getDetail(@RequestParam("id") Long id){        return orderService.getDetail(id);    }}

OrderService.java

package com.ymy.service;import com.ymy.bo.OrderBo;import com.ymy.mapper.OrderMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;@Service@Slf4jpublic class OrderService {    private RedisTemplate redisTemplate;    private OrderMapper orderMapper;    public OrderService(RedisTemplate redisTemplate,OrderMapper orderMapper){        this.redisTemplate = redisTemplate;        this.orderMapper = orderMapper;    }    /**     * 通过id查询订单详情     * @param id     * @return     */    public OrderBo getDetail(Long id) {        //缓存中查询词词订单        OrderBo  orderBo = (OrderBo) redisTemplate.opsForValue().get("order:" + id);        if(orderBo != null ){            log.info("缓存中查询到了信息,直接返回:{}",orderBo);            return orderBo;        }        log.info("前往数据库查询");        orderBo =  orderMapper.getDetail(id);        if(orderBo != null ){           //将数据保存到数据库,有效时间一小时           redisTemplate.opsForValue().set("order:" + id,orderBo,3600,TimeUnit.SECONDS);           log.info("数据已经存入缓存");        }        return orderBo;    }}

OrderMapper.java

package com.ymy.mapper;import com.ymy.bo.OrderBo;import org.apache.ibatis.annotations.Mapper;import org.apache.ibatis.annotations.Select;@Mapperpublic interface OrderMapper {    /**     * 通过订单id查询订单信息     * @param id     * @return     */    @Select(" select id,order_code as orderCode,order_price as orderPrice,peoduct_name as peoductName,create_time as createTime from orders where id = #{id} ")    OrderBo getDetail(Long id);}

RedisConfig.java

package com.ymy.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;@Configurationpublic class RedisConfig {    @Bean    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {        RedisTemplate redisTemplate = new RedisTemplate<>();        redisTemplate.setConnectionFactory(redisConnectionFactory);        // 使用Jackson2JsonRedisSerialize 替换默认序列化        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);        ObjectMapper objectMapper = new ObjectMapper();        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);        // 设置value的序列化规则和 key的序列化规则        redisTemplate.setKeySerializer(new StringRedisSerializer());        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);        redisTemplate.setHashKeySerializer(new StringRedisSerializer());        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);        redisTemplate.afterPropertiesSet();        return redisTemplate;    }}

上面的代码实现的功能很简单,通过订单id查询订单详情,不过查询的循序是先查缓存,如果缓存没有数据,在查询数据库,大致流程图如下:

这个过程很简单,看上去没有什么问题,如果你仔细观察的话就会发现一个致命的问题,就是刚刚说的缓存穿透问题,我们来做个实验。

我在数据库提前添加了一条数据,信息如下:

在这里插入图片描述

正常情况:查询id等于1的订单信息。

第一次:

在这里插入图片描述

2020-04-19 15:55:35.564  INFO 20188 --- [nio-9900-exec-1] com.ymy.service.OrderService             : 前往数据库查询2020-04-19 15:55:35.675  INFO 20188 --- [nio-9900-exec-1] com.ymy.service.OrderService             : 数据已经存入缓存

由于是第一次查询,所以缓存中不会存在数据,请求直接到达了数据库,并且获取到了id为1的数据,并且将数据添加到了缓存。

第二次查询id等于1的数据

2020-04-19 15:57:47.879  INFO 20188 --- [nio-9900-exec-5] com.ymy.service.OrderService             : 缓存中查询到了信息,直接返回:OrderBo(id=1, orderCode=202004191416, orderPrice=3299.00, peoductName=iphone se2, createTime=2020-04-19 14:17:07)

我们发现他直接命中了缓存,直接返回,这是正常情况,那如果非正常情况呢?比如查询的订单id=-1呢?这个时候会发生什么事情?http://localhost:9900/detail?id=-1

看到没有,请求全都进入数据库了,这种情况是肯定不被允许的,如果你的程序中存在这种情况,一定要赶紧修改,否则有可能会让一些心怀不轨的人直接将数据库的服务搞宕机,那这种问题如何解决呢?

解决方案

将空数据存入缓存

什么意思呢?简单点来说,不管数据库中有没有查询到数据,都往缓存中添加一条数据,这样下次请求的时候就会直接在缓存中返回,这种方式比较简单粗暴,我们一起看看如何实现。

代码改造:

OrderService.java

 public OrderBo getDetail(Long id) {        //缓存中查询词词订单        Object obj =  redisTemplate.opsForValue().get("order:" + id);        if(obj != null ){            String data = obj.toString();            log.info("缓存中查询到了信息,直接返回:{}",data);            return  "".equals(data)  ? null : (OrderBo) obj;        }        log.info("前往数据库查询");        OrderBo orderBo =  orderMapper.getDetail(id);        if(orderBo != null ){            //将数据保存到数据库,有效时间一小时           redisTemplate.opsForValue().set("order:" + id,orderBo,3600,TimeUnit.SECONDS);           log.info("数据已经存入缓存");        }else {            redisTemplate.opsForValue().set("order:" + id,"",300,TimeUnit.SECONDS);            log.info("数据库中不存在此数据,但是为了防止缓存穿透,存入一条空数据到缓存中");        }        return orderBo;    }

往缓存中添加数据的时候一定要注意值的问题,请看我这里,我添加的是一个空字符串,并不是null,是因为我判断的条件是缓存中!=null就直接返回,如果你往缓存中添加一条null的数据,这个时候就会和你的判断起冲突,又会进入到数据库了,所以这点需要特别注意,我们来看测试:第一次请求:http://localhost:9900/detail?id=-1

2020-04-19 16:23:21.520  INFO 16596 --- [nio-9900-exec-6] com.ymy.service.OrderService             : 前往数据库查询2020-04-19 16:23:21.577  INFO 16596 --- [nio-9900-exec-6] com.ymy.service.OrderService             : 数据库中不存在此数据,但是为了防止缓存穿透,存入一条空数据到缓存中

第二次请求:http://localhost:9900/detail?id=-1

2020-04-19 16:24:25.855  INFO 16596 --- [nio-9900-exec-9] com.ymy.service.OrderService             : 缓存中查询到了信息,直接返回:

这个时候请求命中了缓存,就不会前往数据库中了,但是这个需要注意一点:空值的过期时间不能设置的太长,什么意思呢?设想一下,我们现在数据库中只有id=1的数据,我们查询id=2也会往缓存中插入一条数据,但是这个时候数据库中新增了一条订单id=2,用户下次查询的时候看到你存储在缓存中中的数据,接直接回了空,但是数据库中明明已经添加了这条数据,这就是为什么过期时间不要设置太久的原因,当然了,我们也需要分情况考虑,比如查询id<=0的,我们都可以考虑永久存入缓存或者设置很长的过期时间,推荐设置很长的过期时间,为什么呢?因为订单id不存在会<=0,但是对于>=0,我们可以将过期时间设置为30秒等等,这个看业务需求即可。

布隆过滤器

布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多,缺点是有一定的误识别率和删除困难。这个算法实现起来比上面第一种稍微复杂一点,这里就不具体说明了,如果感兴趣的话可以百度自行了解一下,不是很难。

缓存击穿

什么是缓存击穿

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db(数据库)。这个也不难理解,和缓存穿透有点像但是性质又不相同,都是缓存中没有数据,请求命中数据库,缓存穿透指的是数据库中不存在的数据,缓存击穿则是指缓存失效的问题。,这种情况不太好模拟,我们可以直接将缓存中数据清空,替代缓存数据过期。代码还是上面的代码,不做任何修改,不过我们不再使用postman测试,而是采用jemter,首先我们删除缓存中的数据,模拟key已经过期,我们查询id=1的订单详细信息,但需要注意的是,我并不是发一个请求,而是100个同时请求,会发生什么呢?线程数:

Http请求信息

聚合报告

我们发现100个并发请求全部成功,异常率为0,接下来就是重点了,控制台会打印什么呢?

这就是缓存击穿,是不是很恐怖,虽然命中数据库的次数不是很多,那是因为我们的并发请求不是很大,像双十一这种并发,如果存在这种问题,数据库可能撑不过3秒就炸了。

解决方案

自动更新

什么是自动更新呢?这个有点类似与jwt的自动刷新token机制,jwt的自动刷新token实现原理大致为:请求的时候判断一下token的剩余有效时间,如果有效时间小于设定的时间,那么jwt将生成一个新的token,然后再将次token重新设置过期时间,并将新的token返回给前端使用,这个也可以参考一下,redis是支持查询某个key剩余有效时间,所以这里我们只需要设定一个时间差,比如3分钟,请求的时候查询的有效时间如果小于3分钟,那么刷新这个key的有效时间,刷新这个操作可以使用异步实现(提高性能)。可能你想到了,这种方式存在缺陷,没错,如果再快失效的3分钟内没有请求,那么缓存中的key将不会被刷新,还是会存在缓存击穿的问题,所以这种方式不是特别推荐

定时刷新

定时刷新有两种方案

第一种:定时任务查询快要过期的key,更新内容,并刷新有效时间,这种比较消耗服务器性能,也不是特别推荐。第二种:延迟队列如果大家了解它的话可能一下就知道我说的是什么意思了,将数据存入缓存的那一刻同时发送一个延迟队列(安指定时间消费),时间小于缓存中key的过期时间,到了指定时间,消费者刷新key的有效时间再发送一个延迟队列,以此循环,这种方式还是不错的,但是实现方式相对于第一种来说就要复杂一点了,他需要依靠消息中间件来完成,如果消息中间件某个时间宕机,那就gg了,虽然这种方式虽然比较推荐,但是成本偏高,因为为了防止消息中间件宕机,我们有可能需要对消息中间件做集群处理。

程序加锁

我个人推荐使用这个,为什么呢?因为它不需要额外的服务器开销,也不需要额外的资源消耗,他仅仅只是让线程串行而已,但是这个时候你可能就会有疑问了,加锁不是会严重影响程序的效率吗?为什么你还推荐这种方式呢?其实并不是所有的锁都会很大的降低程序的性能,这里我们当然不能使用synchronized,原因很简单,他的效率比较慢,不太适合这种情况,我要介绍的这种锁名字为:读写锁。

好了,我们一起来改造一下之前的代码OrderService.java

package com.ymy.service;import com.ymy.bo.OrderBo;import com.ymy.mapper.OrderMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;@Service@Slf4jpublic class OrderService {    private RedisTemplate redisTemplate;    private OrderMapper orderMapper;    private static  final AtomicInteger count = new AtomicInteger(0);    private static final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();    public OrderService(RedisTemplate redisTemplate, OrderMapper orderMapper) {        this.redisTemplate = redisTemplate;        this.orderMapper = orderMapper;    }    /**     * 通过id查询订单详情     *     * @param id     * @return     */    public  OrderBo getDetail(Long id) {        int num = count.incrementAndGet();        //获取读锁        Lock readLock = readWriteLock.readLock();        try {            readLock.lock();            //缓存中查询订单信息            log.info("前往缓存中查询信息,第一次,这是第:{}次请求",num);            Object obj = redisTemplate.opsForValue().get("order:" + id);            if (obj != null) {                String data = obj.toString();                log.info("缓存中查询到了信息,直接返回:{}", data);                return "".equals(data) ? null : (OrderBo) obj;            }            log.info("没有在缓存中获取到数据,即将前往数据库获取,这是第:{}次请求",num);        } finally {            //释放读锁            readLock.unlock();        }        //获取写锁        Lock writeLock = readWriteLock.writeLock();        try{            writeLock.lock();            //缓存中查询订单信息            log.info("第二次前往缓存中查询信息,这是第:{}次请求",num);            Object obj = redisTemplate.opsForValue().get("order:" + id);            if (obj != null) {                String data = obj.toString();                log.info("缓存中查询到了信息,直接返回:{}", data);                return "".equals(data) ? null : (OrderBo) obj;            }            log.info("前往数据库查询,这是第:{}次请求",num);            OrderBo orderBo = orderMapper.getDetail(id);            log.info("数据库返回的数据:{},这是第:{}次请求",orderBo,num);            if (orderBo != null) {                //将数据保存到数据库,有效时间一小时                redisTemplate.opsForValue().set("order:" + id, orderBo, 3600, TimeUnit.SECONDS);                log.info("数据已经存入缓存,这是第:{}次请求",num);            } else {                redisTemplate.opsForValue().set("order:" + id, "", 300, TimeUnit.SECONDS);                log.info("数据库中不存在此数据,但是为了防止缓存穿透,存入一条空数据到缓存中,这是第:{}次请求",num);            }            return orderBo;        }finally {            writeLock.unlock();        }    }}

加了读写锁之后我们一起来看看控制台的输出结果:

这只是其中一部分,由于输出的内容过长我就不全部展示出来了,我们这里需要关注的只有一个,数据库查询了多少次?我们将控制台日志拷贝到notepad++中,搜索“数据库返回的数据”,请看结果:

我们可以看到,查询数据库的操作只有一处,但是查询缓存的确实并发执行的,这就是为什么我推荐使用读写锁的原因,读写锁中读锁和写锁是互斥的,你觉得这样速度还是不够快,能不能读锁和写锁并行?答案是肯定的

缓存雪崩

什么是缓存雪崩

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。为什么会出现大批量的key过期呢?是不是我们设置了相同的过期时间导致的?

解决方案

随机设置过期时间

这个随机时间并不是真正的随机时间,而是指在原来过期时间的基础上生成一个随机时间,这个随机时间比较小,然后两者相加即可

设置永久有效

将一些常用的数据设置成为永久有效,注意哦,是经常使用的而不是全部,这点需要特别注意。

总结

什么是缓存穿透?

同一时刻,大量的并发请求数据库中不存在的信息,他既不会命中缓存,也不会命中数据库,但是他会查找数据库。

什么是缓存击穿?

缓存击穿是指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db(数据库)。

什么是缓存雪崩?

缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机并不是只有上面几种解决方案,这里我只是讲解了几种常用的解决方案,在日常开发中我们可以根据实际的业务需求进行选择,没有最好的,只有最适合自己的,所以不一定要选择最牛逼的解决方案,但是一定要选择最适合项目的解决方案。

作者:卖托儿索的小火柴

原文链接:https://blog.csdn.net/qq_33220089/article/details/105614087

设置log缓存_带你搞明白什么是缓存穿透、缓存击穿、缓存雪崩相关推荐

  1. mysql缓存怎么防止缓存击穿_带你搞明白什么是缓存穿透、缓存击穿、缓存雪崩...

    目录什么是缓存 人在地上走,锅从天上来 缓存穿透什么是缓存穿透 解决方案将空数据存入缓存 布隆过滤器 缓存击穿什么是缓存击穿 解决方案自动更新 定时刷新 程序加锁 缓存雪崩什么是缓存雪崩 解决方案随机 ...

  2. 牛逼,三句话搞懂 Redis 缓存穿透、击穿、雪崩!

    前言 如何有效的理解并且区分 Reids 穿透.击穿和雪崩之间的区别,一直以来都挺困扰我的.特别是穿透和击穿,过一段时间就稀里糊涂的分不清了. 为了有效的帮助笔者自己,以及拥有同样烦恼的朋友们区分这三 ...

  3. Redis 缓存穿透、击穿、雪崩现象及解决方案

    前言 如何有效的理解并且区分 Reids 穿透.击穿和雪崩 缓存穿透 关键词:穿过 Redis 和数据库 当 Redis 和数据库中都没有我们想要的数据时,就需要考虑缓存穿透的问题了.下面这段逻辑大家 ...

  4. Redis缓存知识-穿透、击穿、雪崩

    目录 一.Redis介绍 二.Redis做缓存服务器 三.缓存穿透&击穿&雪崩 1.缓存穿透 2.缓存击穿 3.缓存雪崩 大家好,我是杨叔.每天进步一点点,关注我的微信公众号[程序员杨 ...

  5. Redis应用问题解决(缓存穿透、击穿、雪崩、分布式锁)

    Redis应用问题解决(缓存穿透.击穿.雪崩.分布式锁) 缓存穿透 问题描述 当系统中引入redis缓存后,一个请求进来后,会先从redis缓存中查询,缓存有就直接返回,缓存中没有就去db中查询,db ...

  6. 带你搞明白什么是缓存穿透、缓存击穿、缓存雪崩

    文章开始之前先做个找工作的介绍吧,由于这次疫情影响,我一个UI朋友的公司破产之后他现在处于找工作阶段,一直没有找到自己合适的,三年工作经验左右,坐标深圳,如果有招UI的朋友可以联系我. 作品: htt ...

  7. int** 赋值_一篇文章搞明白Integer、new Integer() 和 int 的概念与区别

    基本概念的区分 1.Integer 是 int 的包装类,int 则是 java 的一种基本数据类型 2.Integer 变量必须实例化后才能使用,而int变量不需要 3.Integer 实际是对象的 ...

  8. webpack打包缓存_【第835期】Webpack 的静态资源持久缓存

    原标题:[第835期]Webpack 的静态资源持久缓存 前言 你现在还在休假吗?早读课节前以web pack漫谈结尾,今年就以web pack开始吧.今日早读文章由众成翻译@yanni4night带 ...

  9. 缓存穿透、击穿、雪崩什么的傻傻分不清楚?看了这篇文后,我明白了

    对于缓存,大家肯定都不陌生,不管是前端还是服务端开发,缓存几乎都是必不可少的优化方式之一.在实际生产环境中,缓存的使用规范也是一直备受重视的,如果使用的不好,很容易就遇到缓存击穿.雪崩等严重异常情景, ...

最新文章

  1. 如何python安装hadoop_使用Python操作Hadoop,Python-MapReduce
  2. android 如何做记住密码
  3. centos7设置成文件服务器,CentOS7服务器架设ftp过程
  4. realloc invalid pointer错误解析
  5. python中大于0的元素全部转化为1,小于0的元素全部转化为0的代码
  6. Spring 5 新功能:函数式 Web 框架
  7. Pymol教程--安装
  8. 在ourdev上看的一个帖子
  9. 彼得·林奇迄今最珍贵的一次演讲
  10. python将姓王的都改成老王_全域明星-第46章:校长姓王,隔壁老王?-爱阅小说网...
  11. vue字符串生成二维码的相关组件
  12. 用python画星空-用python画星空源代码是什么?
  13. 云上部署oracle rac,在青云上部署oracle rac全过程
  14. html5怎么有漂浮的效果,实现元素漂浮在水面特效的jQuery插件
  15. 一场虚拟现实密室逃脱冒险,让你见识科技新加坡
  16. 树莓派python3的opencv下载(编译失败第六步必看)
  17. 解决WIN/MAC平台谷歌浏览器/Chrome添加插件安装时显示程序包无效:CRX_HEADER_INVALID的问题
  18. 路由传参的三种方式(query/params)
  19. least(exp1,exp2,exp3,……,expn)
  20. 广东迅视资管 有温度更要守法度

热门文章

  1. IO:Reactor设计模式
  2. 【Python】青少年蓝桥杯_每日一题_12.11_开关灯问题
  3. 【Python】青少年蓝桥杯_每日一题_6.11_打印星号
  4. android ext3 格式化,怎样将TF卡格式化为EXT分区?
  5. 数据中心在疫情期间发挥的作用
  6. Equinix 位于伦敦的LD8数据中心发生严重故障
  7. 网络的概念与网络的基本分类
  8. 区块链太太太太太难了,我花了一分钟才搞懂!
  9. 亚马逊部分 AWS DNS 系统遭 DDoS 攻击,已达数小时之久
  10. html5 video标签不能播放视频,h5 video 视频不能正常播放