bingfeng

bingfeng 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

日拱一卒,功不唐捐

个人动态

bingfeng 发布了文章 · 11月25日

为了一个HTTP请求问题,差点和iOS干起来

本次斗殴事件起因全部归iOS,为啥这么说,http请求都不会发,瞎写的什么玩意(ps:他应该不会看到...)。

在处理本次冲突中,意外发现了另外一个存在已久的bug,我们先说说这个玩意,再说我们之间的恩怨。因为这是息息相关的。

SpringBoot中的过滤器

过滤器这东西应该很常见了,但是你的过滤器真的起到拦截的作用了,这里就算你起到拦截的作用了,但是你的过滤器能拦截到指定的路径吗?先看一下我原始写法。

谨慎参考:

@WebFilter(filterName = "baseFilter", urlPatterns = "/*")
public class BaseFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) {

        System.out.println("baseFilter 拦截了 /*");
        
        filterChain.doFilter(req, resp);
    }
}

首先这里说下,如果你这是特别单纯的加个@WebFilter就以为ok了,那我告诉你,脸会被打的很疼的。

因为这个注解是servlet的,所以你一定要记得在启动类上加@ServletComponentScan此注解,这样在应用启动的时候,过滤器才会被扫描到。

我们写了一个Controller的接口访问了下,可以看到拦截器确实拦截到了我们的请求。

你以为的只是你以为

我们项目有时候大了,不知道引入了什么东西,有时候会导致这个过滤器呢就无法被注入,看到那行报错呢可能脑子还没反应过来,但是CV大法已经打开了度娘,找到了问题原因,度娘说你加个@Commponent注解好了。然后也确实好了,然后接下来他都如何操作。

@Component
@WebFilter(filterName = "baseFilter", urlPatterns = "/user/*")
public class BaseFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) {

        HttpServletRequest request = (HttpServletRequest) req;

        String url = request.getRequestURL().toString();

        System.out.println(url);
        System.out.println("baseFilter 拦截了 /*");
        
        filterChain.doFilter(req, resp);
    }
}

然而,不巧的是加了@Component注解虽然解决了问题,但是呢urlPatterns拦截的指定路径却没有生效。

我这里是一个pub开头的请求,拦截器拦截的user开头的,然后如下:

他居然将所有的请求给我拦截了下来,不是我想象的那样,那我们该如何解决这种问题呢?往下看同学。

SpringBoot如何注入过滤器

这里我就不列举众多的注入方式了,以免混淆大家,我就直接告诉你们怎么正确注入就ok了,本人已经亲测,而且管理起来很是方便。

过滤器写法

过滤器除了实现Filter之外,不要加任何的东西,就是这么简单。

public class BaseFilter implements Filter {


    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) {

        HttpServletRequest request = (HttpServletRequest) req;

        String url = request.getRequestURL().toString();

        System.out.println(url);
        System.out.println("baseFilter 拦截了 /*");
        
        filterChain.doFilter(req, resp);
    }
}

过滤器注入

我们这里直接通过配置类的方式将过滤器注入,这样呢,我们这里也一目了然,看到我们所有的过滤器,以及过滤器规则。

下面的这些参数都是基本配置,基本都是必填,name你就写过滤器的类名,首字母小写就好了,order就是过滤器的执行顺序,数字越小,越先执行。

这样我们一个完整的过滤器就配置好了。当你再访问/pub接口时,是不会被BaseFilter拦截到的。

这里也推荐大家以后尽量这样去配置。

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<BaseFilter> baseFilter() {
        FilterRegistrationBean<BaseFilter> filterBean = new FilterRegistrationBean<>();
        filterBean.setFilter(new BaseFilter());
        filterBean.setName("baseFilter");
        filterBean.addUrlPatterns("/user/*");
        filterBean.setOrder(1);
        return filterBean;
    }
}

我与iOS的一战

我们先看报的错,再来聊聊这次的锅我是怎么甩的

RequestRejectedException: The request was rejected because the URL was not normalized.

看到没因为网址不标准,导致请求被拒绝。

非说我接口有问题,本来想奋起反抗,看到对方比我身材威猛,想想还是抓到实质性证据在甩他吧。

既然说请求网址不正确,我猜测就是请求路径中是不是有什么猫腻,那我们就抓包呗。

最后在我们各种手段之下拿到了真凭实据。诸位法官请看:

他的请求路径:http://127.0.0.1:8080//user/list

他的请求路径中出现了双斜杠,这样肯定报错啊。这里需要说明下,报错是因为引入了Security安全框架。

既然已经确定问题,那我必须奋起反抗,找他甩锅,当他看到这个时候,对吧自己也无话可说,只能默默的把锅背上。

就这样我这次又顺利的甩锅成功。

解决与反思

虽然锅甩出去了,但是问题还是要解决的。

其实按正常逻辑来说,不管我们引入了什么东西,只要请求路径正确,及时路径中出现再多的斜杠,我们也应该做好处理,不能影响用户的访问。所以我们就通过过滤器就行一个处理。

public class UriFormatFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain)
            throws ServletException, IOException {

        // 路径隔离符号
        String separateSymbol = "/";

        String uri = req.getRequestURI();
        StringBuilder newUrl = new StringBuilder();

        String[] split = uri.split(separateSymbol);

        for (String s : split) {
            if (StringUtils.isNotBlank(s)) {
                newUrl.append(separateSymbol).append(s);
            }
        }

        req = new HttpServletRequestWrapper(req) {
            @Override
            public String getRequestURI() {
                return newUrl.toString();
            }
        };
        filterChain.doFilter(req, res);
    }
}

最后将过滤器注入

这里order为啥写-100,如果你写1,你以为它会是第一个执行,其实不然,在执行他之前,可能框架的一些过滤器会先执行,所以为了保险起见,我们就设置为-100,确保请求进来之后先走它。

@Bean
public FilterRegistrationBean<UriFormatFilter> uriFormatFilter() {
    FilterRegistrationBean<UriFormatFilter> filterBean = new FilterRegistrationBean<>();
    filterBean.setFilter(new UriFormatFilter());
    filterBean.setName("uriFormatFilter");
    filterBean.addUrlPatterns("/*");
    filterBean.setOrder(-100);
    return filterBean;
}

注意

如果你在过滤器中注意一些Mapper、Service之类的话,可能会出现问题,调用的时候被注入的对象可能是个null,这就涉及到类的加载顺序,我就不在这里bibi了,真有人遇到了再说。反正我已经解决了[Doge]。

参考文章:

https://blog.csdn.net/chenmen...
https://blog.csdn.net//qq_300...

更多精彩内容请关注微信公众号:一个程序员的成长

查看原文

赞 2 收藏 0 评论 1

bingfeng 发布了文章 · 11月17日

我的服务器接连被黑客攻击,我好难

最近在几台测试服务器上跑一些业务数据,但是过了几天服务器突然变的奇慢无比,敲个命令就像卡壳一样,有时候甚至都连接不上,最开始我以为是网络问题,就强行kill掉进程,重新跑一下进程,最后实在受不了,就上阿里云后台说重启下服务器吧,结果看到CPU的占用率已经到达了100%。

这是CPU是恢复正常之后截图

看到这样我以为是因为我跑了大量的数据导致CPU飙升的,然后我就kill到了进程,并且重启了服务器,启动之后CPU正常,我以为就是我跑数据导致的,此后我就没用这台服务器跑数据了,我就单纯的以为这就算处理好了,没想到等我过几天部署测试包的时候发现,又是奇慢无比,看了下CPU占用率又是99.9%,事实证明我还是太年轻了。

终于忍无可忍,就深究下吧,先用linux命令(top)查看下,到底是什么占用了这么多CPU资源,结果如下图:

看到的瞬间第一感觉就是,这是啥玩意,这是谁部署的。问了下平时身旁的背锅侠,好像也不是他弄的,看来这次这锅是甩不了了,那就只能...

What?中病毒了?

根据过往的经验,这玩意不应该是点了网页上的小姐姐才会发生的事情吗?我这为什么也就中毒了。

这东西是啥

既然已经中毒了,那就来看看这是什么东西吧。

挖矿病毒,大家身在同一个工地都应该或多或少都听过挖矿吧,要是挖到个币,就不用苦逼写文章了,话说回来,要想挖币需要很强的计算资源,那么也就需要众多的服务器来支撑,这里面有些逼呢又不想投入太多,只能通过一些恶毒的手段,将脚本植入的我们的服务器,比如我们需要安装一个Redis,那么像我英文不太好的人,可能第一时间不是去官网,而是找度娘,如果你正好找的资源里面被人植入了这种东西,那么很不凑巧,你的服务器可能要帮别人搞点东西了。

如何处理这种病毒

既然中了这种病毒,导致我们的服务器很卡,那么肯定要将它杀死,可能没怎么接触过Linux的同学,已经考虑重装镜像了。

其实大可不必。

首先呢我们找到此进程将其kill掉。

接下来删除kdevtmpfsi文件,一般在tmp目录下

还有一个文件(kinsing)我们也要将其杀死删掉

这里需要注意,我试了几台服务器kinsing文件可能存在不同的位置,但是我们可以通过上面的方式看到文件路径,将其找到删除就好。

这个时候我们通过top查看CPU的使用率,可以发现已经正常了


就在我以为万事大吉的时候,现实又给了我沉痛的一击,没过几分钟CPU使用率又到了99.96%,我要崩溃了。

跟度娘经过深入交流之后,终于知道了问题所在。

查看服务器的定时任务,crontab -l,大概会看到如下的任务,没有就不用管了,你可以将此ip查一下,一般都是国外的ip。

我们将这些定时任务删除即可,这个链接就是在我们kill到进程、删除文件之后进行下载,然后通过脚本再跑起来。

这也就是为什么我明明杀死了病毒,没过多久又出现了的原因。

到这里我们已经完全处理到此病毒了,如果你用的是阿里云ECS,当遇到这种东西的时候,其实会短信通知你,只不过当时太年轻没怎么在意,另外服务器端口默认是22,自己最好改个端口,不然很容易被恶人攻击。

现在服务器敲起来贼爽,再也不卡顿了。


更多精彩内容请关注微信公众号:一个程序员的成长

查看原文

赞 7 收藏 2 评论 7

bingfeng 赞了文章 · 11月5日

岁月如何才能静好

静中有争,稳中有急

欲争先静,宁静致运
急事需稳,稳中求胜

胜中有生,败中有文

欲胜先生,活着就是胜利
败亦求文,挫折不足为训

我今天47,找了第三份工作,coding依旧。

第一次写程序在88年,忘不了 a=a+1 带来的惊奇。

1995年,大学时,通宵玩三国,有一次良心发现,用两个通宵用 Matlab 3 改写了学校 IBM MainFrame实现了机器里的有限元的程序,同样的程序,但在PC 386上跑。传言那个IBM竞有夸张的 2G 内存,三个计算机教室共用同一台机器,我那可怜的 386 只有 4M内存,还是扩容后的。

工作后第一年抄了一个公式编辑器,把数学公式编辑+计算求值+存到Metafile里,领导给涨了工资,内心有悔。

换了个工作,东拼西凑,抄了个ORM+CodeGenerator, 靠这个混了一段时间,一混混到了2019年。

2019年,觉得老罗挺可怜,买了他两个手机和一些周边,可惜还是没挡住他去卖烟...

不由让我想起蓝淀厂,火器营,清水河边...

就快新年了,去年立的Flag倒下一片,也不都怨中美贸易战,自己懒的事我就不提了...

新年再立一愿,搞个自己的网站。做出来再说,做不出来就当我没说!

本文参与了 SegmentFault思否征文「2019 总结」,欢迎正在阅读的你也加入。
查看原文

赞 5 收藏 0 评论 0

bingfeng 回答了问题 · 10月30日

为什么说二级索引不唯一,不唯一是指什么不唯一呢?

这就涉及到聚簇索引和非聚簇索引了,聚簇索引主键id和数据是存储在一行的,但是非聚簇索引,也就是我们自己创建的索引,它的索引和数据是分开的,主键id是不可能重复的,所以对于聚簇索引来说那就是唯一的,但是对于二级普通索引(聚簇索引)来说,创建的索引是非常有可能出现重复值得,所以二级索引是不唯一的,但是如果你的索引是唯一索引,那么这个索引就是唯一的。

个人理解。

关注 4 回答 3

bingfeng 赞了文章 · 9月28日

Mybatis批量更新三种方式

Mybatis实现批量更新操作
方式一:

<update id="updateBatch"  parameterType="java.util.List">  
    <foreach collection="list" item="item" index="index" open="" close="" separator=";">
        update tableName
        <set>
            name=${item.name},
            name2=${item.name2}
        </set>
        where id = ${item.id}
    </foreach>      
</update>

但Mybatis映射文件中的sql语句默认是不支持以" ; " 结尾的,也就是不支持多条sql语句的执行。所以需要在连接mysql的url上加 &allowMultiQueries=true 这个才可以执行。
方式二:

<update id="updateBatch" parameterType="java.util.List">
        update tableName
        <trim prefix="set" suffixOverrides=",">
            <trim prefix="c_name =case" suffix="end,">
                <foreach collection="list" item="cus">
                    <if test="cus.name!=null">
                        when id=#{cus.id} then #{cus.name}
                    </if>
                </foreach>
            </trim>
            <trim prefix="c_age =case" suffix="end,">
                <foreach collection="list" item="cus">
                    <if test="cus.age!=null">
                        when id=#{cus.id} then #{cus.age}
                    </if>
                </foreach>
            </trim>
        </trim>
        <where>
            <foreach collection="list" separator="or" item="cus">
                id = #{cus.id}
            </foreach>
        </where>
</update>

这种方式貌似效率不高,但是可以实现,而且不用改动mysql连接
效率参考文章:https://blog.csdn.net/xu19166...
方式三:
临时改表sqlSessionFactory的属性,实现批量提交的java,但无法返回受影响数量。

public int updateBatch(List<Object> list){
        if(list ==null || list.size() <= 0){
            return -1;
        }
        SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean("sqlSessionFactory");
        SqlSession sqlSession = null;
        try {
            sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
            Mapper mapper = sqlSession.getMapper(Mapper.class);
            int batchCount = 1000;//提交数量,到达这个数量就提交
            for (int index = 0; index < list.size(); index++) {
                Object obj = list.get(index);
                mapper.updateInfo(obj);
                if(index != 0 && index%batchCount == 0){
                    sqlSession.commit();
                }                    
            }
            sqlSession.commit();
            return 0;
        }catch (Exception e){
            sqlSession.rollback();
            return -2;
        }finally {
            if(sqlSession != null){
                sqlSession.close();
            }
        }
        
}

其中 SpringContextUtil 是自己定义的工具类 用来获取spring加载的bean对象,其中getBean() 获得的是想要得到的sqlSessionFactory。Mapper 是自己的更具业务需求的Mapper接口类,Object是对象。
总结
方式一 需要修改mysql的连接url,让全局支持多sql执行,不太安全
方式二 当数据量大的时候 ,效率明显降低
方式三 需要自己控制,自己处理,一些隐藏的问题无法发现。

附件:SpringContextUtil.java

@Component
public class SpringContextUtil implements ApplicationContextAware{

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    public static Object getBean(Class T){
        try {
            return applicationContext.getBean(T);
        }catch (BeansException e){
            return null;
        }
    }

    public static Object getBean(String name){
        try {
            return applicationContext.getBean(name);
        }catch (BeansException e){
            return null;
        }
    }
}
查看原文

赞 5 收藏 4 评论 0

bingfeng 发布了文章 · 9月10日

Executors使用不当引起的内存溢出

线上服务内存溢出

这周刚上班突然有一个项目内存溢出了,排查了半天终于找到问题所在,在此记录下,防止后面再次出现类似的情况。

先简单说下当出现内存溢出之后,我是如何排查的,首先通过jstack打印出堆栈信息,然后通过分析工具对这些文件进行分析,根据分析结果我们就可以知道大概是由于什么问题引起的。

关于jstack如何使用,大家可以先看看这篇文章 jstack的使用

问题排查

下面是我打印出来的信息,大部分都是这个

"http-nio-8761-exec-124" #580 daemon prio=5 os_prio=0 tid=0x00007fbd980c0800 nid=0x249 waiting on condition [0x00007fbcf09c8000]
   java.lang.Thread.State: TIMED_WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x00000000f73a4508> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
        at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:215)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2078)
        at java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:467)
        at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:85)
        at org.apache.tomcat.util.threads.TaskQueue.poll(TaskQueue.java:31)
        at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1073)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:748)

看到了如上信息之后,大概可以看出是由于线程池的使用不当导致的,那么根据信息继续往下看,看到ThreadPoolExecutor那么就可以知道这肯定是创建了线程池,那么我们就在代码里找,哪里创建使用了线程池,我就找到这么一段代码。

public class ThreadPool {
    private static ExecutorService pool;

    private static long logTime = 0;

    public static ExecutorService getPool() {
        if (pool == null) {
            pool = Executors.newFixedThreadPool(20);
        }
        return pool;
    }
}

乍一看,可能写的同学是想把这当一个全局的线程池用,所有的业务凡是用到线程的都会使用这个类,为了统一管理线程,想法没什么毛病,但是这样写确实有点子毛病。

newFixedThreadPool分析

上面使用了Executors.newFixedThreadPool(20)创建了一个固定的线程池,我们先分析下newFixedThreadPool是怎么样的一个流程。

一个请求进来之后,如果核心线程有空闲线程直接使用核心线程中的线程执行任务,不会添加到阻塞队列中,如果核心线程满了,新的任务会添加到阻塞队列,直到队列加满再开线程,直到maxPoolSize之后再触发拒绝执行策略

了解了流程之后我们再来看newFixedThreadPool的代码实现。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
    if (capacity <= 0) throw new IllegalArgumentException();
    // 任务阻塞队列的初始容量
    this.capacity = capacity;
    last = head = new Node<E>(null);
}

定位问题

看到了这里不知道你是否知道了此次引起内存泄漏的原因,其实就是因为阻塞队列的容量过大

如果不手动的指定阻塞队列的大小,那么它默认是Integer.MAX_VALUE,我们的线程池只有20个线程可以处理任务,其他的请求全部放到阻塞队列中,那么当涌入大量的请求之后,阻塞队列一直增加,你的内存配置又非常紧凑的话,那么是很容易出现内存溢出的。

我们的业务是在APP启动的时候,会使用线程池去检查用户的一些配置,应用的启动量还是非常大的而且给的内存配置也不是很足,所以运行一段时间后,部分容器就出现了内存溢出的情况。

如何正确的创建线程池

以前其实没太在意这种问题,都是使用Executors去创建线程,但是这样确实会存在一些问题,就像这些的内存泄漏,所以一般不要使用Executors去创建线程,使用ThreadPoolExecutor进行创建,其实Executors底层也是使用ThreadPoolExecutor进行创建的。

使用ThreadPoolExecutor创建需要自己指定核心线程数、最大线程数、线程的空闲时长以及阻塞队列。

3种阻塞队列
  • ArrayBlockingQueue:基于数组的先进先出队列,有界
  • LinkedBlockingQueue:基于链表的先进先出队列,有界
  • SynchronousQueue:无缓冲的等待队列,无界

我们使用了有界的队列,那么当队列满了之后如何处理后面进入的请求,我们可以通过不同的策略进行设置。

4种拒绝策略
  • AbortPolicy:默认,队列满了丢任务抛出异常
  • DiscardPolicy:队列满了丢任务不异常
  • DiscardOldestPolicy:将最早进入队列的任务删,之后再尝试加入队列
  • CallerRunsPolicy:如果添加到线程池失败,那么主线程会自己去执行该任务
在创建之前,先说下我最开始的版本,因为队列是固定的,最开始我们不知道有拒绝策略,所以在队列满了之后再添加的话会出现异常,我就在异常里面睡眠了1秒,等待其他的线程执行完毕获取空闲连接,但是还是会有部分不能得到执行。

接下来我们来创建一个容错率比较高的线程池。

public class WordTest {

    public static void main(String[] args) throws InterruptedException {

        System.out.println("开始执行");

        // 阻塞队列容量声明为100个
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(10, 10,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(100));

        // 设置拒绝策略
        executorService.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 空闲队列存活时间
        executorService.setKeepAliveTime(20, TimeUnit.SECONDS);

        List<Integer> list = new ArrayList<>(2000);

        try {
            // 模拟200个请求
            for (int i = 0; i < 200; i++) {
                final int num = i;
                executorService.execute(() -> {
                    System.out.println(Thread.currentThread().getName() + "-结果:" + num);
                    list.add(num);
                });
            }
        } finally {
            executorService.shutdown();
            executorService.awaitTermination(10, TimeUnit.SECONDS);
        }
        System.out.println("线程执行结束");
    }
}

思路:我声明了100容量的阻塞队列,模拟了一个200的请求,很显然肯定有部分请求进入不了队列,但是我使用了CallerRunsPolicy策略,当队列满了之后,使用主线程去进行处理,这样就不会出现有部分请求得不到执行的情况,也不会因为因为阻塞队列过大导致内存溢出的情况。

如果还有什么更好地写法欢迎各位指教!

通过测试200个请求全部得到执行,有3个请求由主线程进行了处理。

总结

如何更好的创建线程池上面已经说过了,关于线程池在业务中的使用,其实我们这种全局的思路是不太好的,因为如果从全局考虑去创建线程池,是很难把控的,因为你无法准确地评估所有的请求加起来会有多大的量,所以最好是每个业务创建独立的线程池进行处理,这样是很容易评估量化的。

另外创建的时候,最好评估下大概每秒的请求量有多少,然后来合理的初始化线程数和队列大小。

参考文章:<br/>
https://www.cnblogs.com/muxi0...

更多精彩内容请关注微信公众号:一个程序员的成长

查看原文

赞 7 收藏 5 评论 0

bingfeng 发布了文章 · 9月6日

导致MySQL索引失效的几种常见写法

最近一直忙着处理原来老项目遗留的一些SQL优化问题,由于当初表的设计以及字段设计的问题,随着业务的增长,出现了大量的慢SQL,导致MySQL的CPU资源飙升,基于此,给大家简单分享下这些比较使用的易于学习和使用的经验。

这次的话简单说下如何防止你的索引失效。

再说之前我先根据我最近的经验说下我对索引的看法,我觉得并不是所以的表都需要去建立索引,对于一些业务数据,可能量比较大了,查询数据已经有了一点压力,那么最简单、快速的办法就是建立合适的索引,但是有些业务可能表里就没多少数据,或者表的使用频率非常不高的情况下是没必要必须要去做索引的。就像我们有些表,2年了可能就10来条数据,有索引和没索引性能方面差不多多少。

索引只是我们优化业务的一种方式,千万为了为了建索引而去建索引。

下面是我此次测试使用的一张表结构以及一些测试数据

`CREATE TABLE user` (
  id int(5) unsigned NOT NULL AUTO_INCREMENT,
  create_time datetime NOT NULL,
  name varchar(5) NOT NULL,
  age tinyint(2) unsigned zerofill NOT NULL,
  sex char(1) NOT NULL,
  mobile char(12) NOT NULL DEFAULT '',
  address char(120) DEFAULT NULL,
  height varchar(10) DEFAULT NULL,
  PRIMARY KEY (id),
  KEY idx_createtime (create_time) USING BTREE,
  KEY idx_name_age_sex (name,sex,age) USING BTREE,
  KEY idx_ height (height) USING BTREE,
  KEY idx_address (address) USING BTREE,
  KEY idx_age (age) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=261 DEFAULT CHARSET=utf8;
复制代码``

`INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight`) VALUES (1, '2019-09-02 10:17:47', '冰峰', 22, '男', '1', '陕西省咸阳市彬县', '175');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (2, '2020-09-02 10:17:47', '松子', 13, '女', '1', NULL, '180');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (3, '2020-09-02 10:17:48', '蚕豆', 20, '女', '1', NULL, '180');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (4, '2020-09-02 10:17:47', '冰峰', 20, '男', '17765010977', '陕西省西安市', '155');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (255, '2020-09-02 10:17:47', '竹笋', 22, '男', '我测试下可以储存几个中文', NULL, '180');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (256, '2020-09-03 10:17:47', '冰峰', 21, '女', '', NULL, '167');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (257, '2020-09-02 10:17:47', '小红', 20, '', '', NULL, '180');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (258, '2020-09-02 10:17:47', '小鹏', 20, '', '', NULL, '188');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (259, '2020-09-02 10:17:47', '张三', 20, '', '', NULL, '180');
INSERT INTO bingfeng.user(idcreate_timenameagesexmobileaddressheight) VALUES (260, '2020-09-02 10:17:47', '李四', 22, '', '', NULL, '165');
复制代码``

单个索引

1、使用!= 或者 <> 导致索引失效

`SELECT * FROM user WHERE name` != '冰峰';
复制代码``

我们给name字段建立了索引,但是如果!= 或者 <> 这种都会导致索引失效,进行全表扫描,所以如果数据量大的话,谨慎使用

可以通过分析SQL看到,type类型是ALL,扫描了10行数据,进行了全表扫描。<>也是同样的结果。

2、类型不一致导致的索引失效

在说这个之前,一定要说一下设计表字段的时候,千万、一定、必须要保持字段类型的一致性,啥意思?比如user表的id是int自增,到了用户的账户表user_id这个字段,一定、必须也是int类型,千万不要写成varchar、char什么的骚操作。

`SELECT * FROM user` WHERE height= 175;
复制代码``

这个SQL诸位一定要看清楚,height表字段类型是varchar,但是我查询的时候使用了数字类型,因为这个中间存在一个隐式的类型转换,所以就会导致索引失效,进行全表扫描。

现在明白我为啥说设计字段的时候一定要保持类型的一致性了不,如果你不保证一致性,一个int一个varchar,在进行多表联合查询(eg: 1 = '1')必然走不了索引。

遇到这样的表,里面有几千万数据,改又不能改,那种痛可能你们暂时还体会。

少年们,切记,切记。

3、函数导致的索引失效

`SELECT * FROM user` WHERE DATE(create_time) = '2020-09-03';
复制代码``

如果你的索引字段使用了索引,对不起,他是真的不走索引的。

4、运算符导致的索引失效

`SELECT * FROM user` WHERE age - 1 = 20;
复制代码``

如果你对列进行了(+,-,*,/,!), 那么都将不会走索引。

5、OR引起的索引失效

`SELECT * FROM user WHERE name` = '张三' OR height = '175';
复制代码``

OR导致索引是在特定情况下的,并不是所有的OR都是使索引失效,如果OR连接的是同一个字段,那么索引不会失效,反之索引失效。

6、模糊搜索导致的索引失效

`SELECT * FROM user WHERE name` LIKE '%冰';
复制代码``

这个我相信大家都明白,模糊搜索如果你前缀也进行模糊搜索,那么不会走索引。

7、NOT IN、NOT EXISTS导致索引失效

`SELECT s.* FROM user s WHERE NOT EXISTS (SELECT * FROM user u WHERE u.name = s.name AND u.name` = '冰峰')
复制代码``

`SELECT * FROM user WHERE name` NOT IN ('冰峰');
复制代码``

这两种用法,也将使索引失效。但是NOT IN 还是走索引的,千万不要误解为 IN 全部是不走索引的。我之前就有误解(丢人了...)。

符合索引

1、最左匹配原则

`EXPLAIN SELECT * FROM user` WHERE sex = '男';
复制代码``

`EXPLAIN SELECT * FROM user` WHERE name = '冰峰' AND sex = '男';
复制代码``

测试之前,删除其他的单列索引。

啥叫最左匹配原则,就是对于符合索引来说,它的一个索引的顺序是从左往右依次进行比较的,像第二个查询语句,name走索引,接下来回去找age,结果条件中没有age那么后面的sex也将不走索引。

注意:

`SELECT * FROM user WHERE sex = '男' AND age = 22 AND name` = '冰峰';
复制代码``

可能有些搬砖工可能跟我最开始有个误解,我们的索引顺序明明是name、sex、age,你现在的查询顺序是sex、age、name,这肯定不走索引啊,你要是自己没测试过,也有这种不成熟的想法,那跟我一样还是太年轻了,它其实跟顺序是没有任何关系的,因为mysql的底层会帮我们做一个优化,它会把你的SQL优化为它认为一个效率最高的样子进行执行。所以千万不要有这种误解。

2、如果使用了!=会导致后面的索引全部失效

`SELECT * FROM user WHERE sex = '男' AND name` != '冰峰' AND age = 22;
复制代码``

我们在name字段使用了 != ,由于name字段是最左边的一个字段,根据最左匹配原则,如果name不走索引,后面的字段也将不走索引。

关于符合索引导致索引失效的情况能说的目前就这两种,其实我觉得对于符合索引来说,重要的是如何建立高效的索引,千万不能说我用到那个字段我就去建立一个单独的索引,不是就可以全局用了嘛。这样是可以,但是这样并没有符合索引高效,所以为了成为高级的搬砖工,我们还是要继续学习,如何创建高效的索引。

作者:一个程序员的成长
链接:https://juejin.im/post/686927...
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

查看原文

赞 26 收藏 18 评论 8

bingfeng 发布了文章 · 5月27日

@RequestBody参数已经被读取,究竟是何原因?

不知道你们有没有对用户输入的东西进行过敏感校验,如果不进行校验,用户属于一些攻击脚本,那么我们的服务就挂逼啦!所以我们首先需要通过过滤器将用户的数据读出来进行安全校验,这里面涉及到一个动作,就是需要将用户的数据在过滤器中读出来,进行校验,通过之后再放行。

问题

如果我们的数据是get请求倒还好,但是如果是一些数据量比较大,我们需要通过post json的方式来说传递数据的时候,这个时候其实是通过流的方式传递的,如果在过滤器中将参数读取出来之后,然后放行,等到到Servlet的时候,@RequestBody是无法获取到数据的,因为post json使用流传递,流被读取之后就不存在了,所以我们在过滤器中读取之后,@ReqeustBody自然就读不到数据了,同时会报如下一个错误。

  • 在过滤器中读取body中的数据
@WebFilter
@Slf4j
public class CheckUserFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        // 在过滤器中读取数据
        BufferedReader reader = request.getReader();

        StringBuilder sb = new StringBuilder();

        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        reader.close();

        System.out.println(sb.toString());

        filterChain.doFilter(request, res);
    }
}
  • 出现异常,就是说内容已经被读取了,你不能调用了
{    "id":"1",    "username":"bingfeng"}
java.lang.IllegalStateException: UT010003: Cannot call getInputStream(), getReader() already called
    at io.undertow.servlet.spec.HttpServletRequestImpl.getInputStream(HttpServletRequestImpl.java:666)
    at javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:152)
    at javax.servlet.ServletRequestWrapper.getInputStream(ServletRequestWrapper.java:152)

解决

  • HttpServletRequestWrapper

那么出现这种问题怎么办呢?能不能通过一个中间的变量将这些数据保存下来,然后我们就可以一直读取了,这样不就解决了这个问题了吗?那保存在哪里呢?这个时候 HttpServletRequestWrapper 就排上用场了。

这个其实你可以把它理解为Request的包装类,Reqeust中有的方法它都有,我们通过继承这个类,重写该类中的方法,将body中的参数保存一个byte数组中,然后放行的时候将这个包装类传递进去,不就可以一直拿到参数了?

  • 封装Request类
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] body;

    /**
     * 所有参数的集合
     */
    private Map<String, String[]> parameterMap;


    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        BufferedReader reader = request.getReader();
        body = readBytes(reader);
        parameterMap = request.getParameterMap();
    }


    @Override
    public BufferedReader getReader() throws IOException {

        ServletInputStream inputStream = getInputStream();

        if (null == inputStream) {
            return null;
        }

        return new BufferedReader(new InputStreamReader(inputStream));
    }

    @Override
    public Enumeration<String> getParameterNames() {
        Vector<String> vector = new Vector<>(parameterMap.keySet());
        return vector.elements();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {

        if (body == null) {
            return null;
        }

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream() {

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener listener) {

            }

            @Override
            public int read() throws IOException {
                return bais.read();
            }
        };
    }

    /**
     * 通过BufferedReader和字符编码集转换成byte数组
     *
     * @param br
     * @return
     * @throws IOException
     */
    private byte[] readBytes(BufferedReader br) throws IOException {
        String str;
        StringBuilder retStr = new StringBuilder();
        while ((str = br.readLine()) != null) {
            retStr.append(str);
        }
        if (StringUtils.isNotBlank(retStr.toString())) {
            return retStr.toString().getBytes(StandardCharsets.UTF_8);
        }
        return null;
    }
}
  • 将过滤器改造
@WebFilter
@Slf4j
public class CheckUserFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain) throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest) req;

        BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper(request);

        // 从Request的包装类中读取数据
        BufferedReader reader = requestWrapper.getReader();

        StringBuilder sb = new StringBuilder();

        String line;
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        reader.close();

        System.out.println(sb.toString());

        filterChain.doFilter(requestWrapper, res);
    }
}

经过这样的配置之后,我们即使在过滤器中获取了参数,请求也会到达Servlet。

如果基础知识IO那块不是很扎实的话,第一眼看到这个问题确实挺懵逼的。我也是百度之后解决的,确实值得记录一下,有时候我们会对所有请求进来的参数进行保存输出什么的,这个时候如果是post json数据的话,如果不是特别明白,可能也会出现这种问题。

<p style="text-align:center;font-weight:bold;color:#0e88eb;font-size:20px">日拱一卒,功不唐捐</p>

<p style="text-align:center;font-weight:bold;color:#773098;font-size:16px">更多内容请关注</p>

查看原文

赞 1 收藏 1 评论 0

bingfeng 发布了文章 · 5月19日

RabbitMQ发布订阅模式,同步用户数据

前几篇我们介绍了如果通过RabbitMQ发布一个简单的消息,再到工作队列,多个消费者进行消费,最后再到工作队列的分发与消息的应答机制(ACK);

之前我们分享的这几种模式,都是被消费之后就从队列中被删除了,理想状态下不会被重复消费,试想我们另外一种场景,比如我之前做的小说业务,用户在登录成功后,需要将临时账户的金币和书架的书籍信息同步到正式账户。

如果我们跟登录融合在一块,登录成功之后,如果用户账户或者书架同步失败,那么势必影响我们整个登录的体验。为了更好地做到用户无感知,不需要用户做更多的操作,那么我们就使用消息队列的方式,来进行异步同步。

发布订阅模式

这就是我们一个用户数据同步的流程图,也是RabbitMQ发布订阅的流程图,大家可能注意到了中间怎么多了一个交换机

这里要注意,使用发布订阅模式,这里必须将交换机与队列进行绑定,如果不绑定,直接发送消息,这个消息是不会发送到任何队列的,更不会被消费。

交换机种类

交换机总共分四种类型:分别是direct、topic、headers、fanout。这次我们主要讲fanout,因为这是我们本次需要用到的交换机类型。

fanout顾名思义就是广播模式。它会把消息推送给所有订阅它的队列。

代码

生产者

public class Send {

    /**
     * 交换机名称
     */
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 获取连接
        Connection connection = MQConnectUtil.getConnection();

        // 创建通道
        Channel channel = connection.createChannel();

        // 声明交换机  fanout:分发模式,分裂
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");

        // 消息内容
        String msg = "我是一个登录成功的消息";

        // 发送消息
        channel.basicPublish(EXCHANGE_NAME, "", null, msg.getBytes());

        System.out.println("消息发送成功:" + msg);

        channel.close();
        connection.close();
    }
}

消费者-同步账户

public class Consumer1 {

    /**
     * 交换机名称
     */
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    private final static String QUEUE_NAME = "test_topic_publish_account";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 获取连接
        Connection connection = MQConnectUtil.getConnection();

        // 创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 将队列绑定到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 保证一次只接收一个消息,保证rabbitMQ每次将消息发送给闲置的消费者
        channel.basicQos(1);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @SneakyThrows
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {

                String msg = new String(body, StandardCharsets.UTF_8);

                System.out.println("同步账户[1]:" + msg);

                Thread.sleep(1000);

                // 手动应答
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        // 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

消费者-同步书架

public class Consumer2 {

    /**
     * 交换机名称
     */
    private final static String EXCHANGE_NAME = "test_exchange_fanout";

    private final static String QUEUE_NAME = "test_topic_publish_book_case";

    public static void main(String[] args) throws IOException, TimeoutException {

        // 获取连接
        Connection connection = MQConnectUtil.getConnection();

        // 创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 将队列绑定到交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 保证一次只接收一个消息,保证rabbitMQ每次将消息发送给闲置的消费者
        channel.basicQos(1);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @SneakyThrows
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body)
                    throws IOException {

                String msg = new String(body, StandardCharsets.UTF_8);

                System.out.println("同步书架[2]:" + msg);

                Thread.sleep(1000);

                // 手动应答
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        // 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);
    }
}

总结

那么基于这样的需要同步用户数据的需求,那么为了保证各数据同步之间互不影响,降低耦合性,那么我们就可以使用多个队列,进行用户数据的同步。提升整个系统的高可用。

日拱一卒,功不唐捐

更多内容请关注

查看原文

赞 1 收藏 1 评论 0

bingfeng 发布了文章 · 5月15日

RabbitMQ工作队列之公平分发消息与消息应答(ACK)

上篇文章中,我们讲了工作队列轮询的分发模式,该模式无论有多少个消费者,不管每个消费者处理消息的效率,都会将所有消息平均的分发给每一个消费者,也就是说,大家最后各自消费的消息数量都是一样多的。由此也就引发我们今天要介绍的公平分发模式。

消息应答(ACK)

消息丢失

我们之前的所有代码,如果消息队列将消息分发给消费者,那么就会从队列中删除,如果在我们处理任务的过程中,处理失败或者服务器宕机,那么这条消息肯定得不到执行,就会出现丢失。

我们所设想的如果任务在处理的过程中,如果服务器宕机等原因造成消息未被正常消费,那么必须分发给其他的消费者再次进行消费,这样及时服务器宕机也不会丢失任何的消息了。

ACK

所以ACK,就是消息应答机制,我们之前写的代码都是开启了自动应答,所以如果我们的消息没被正常消费,就会丢失。

要想确保消息不丢失,就必须将ACK自动应答关闭掉,在我们处理消息的流程中,如果消息正常被处理,那么最后进行手动应答,告诉队列我们正常消费了消息。

超时

RabbitMQ它是没有我们平常所见到的超时时间限制的,只要当消费者服务宕机,消息才会被重新分发,哪怕处理这条消息需要花费很长的时间。

公平分发模式

缺陷

我们提供多个消费者,目的就是为了提高系统的性能,提升系统处理任务的速度,如果将消息平均的分发给每个消费者,那么处理消息快的服务是不是会空闲下来,而处理慢的服务可能会阻塞等待处理,这样的场景是我们不愿意看到的。所以有了今天要说的分发模式,公平分发

能者多劳

所谓的公平分发,其实用能者多劳描述更为贴切,根据名字就可以知道,谁有能力处理更多的任务,那么就交给谁处理,防止消息的挤压。

那么想要实现公平分发,那么必须要将自动应答改为手动应答。这是公平分发的前提。

代理

消息生产者

public class Send {

    public static final String QUEUE_NAME = "test_word_queue";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {

        // 获取连接
        Connection connection = MQConnectUtil.getConnection();

        // 创建通道
        Channel channel = connection.createChannel();

        // 声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        for (int i = 0; i < 10; i++) {

            String msg = "消息:" + i;

            channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());

            Thread.sleep(i * 20);

            System.out.println(msg);
        }

        channel.close();
        connection.close();
    }
}

消费者1

我们在消费者中设置了channel.basicQos(1);这样一个参数,这个意思就是表示,此消费者每次最多只接收一条消息进行处理,只有将消息处理结束,手动应答之后,下一条消息才会被分发进来。

public class Consumer1 {

    public static final String QUEUE_NAME = "test_word_queue";

    public static void main(String[] args) throws Exception {

        // 获取连接
        Connection connection = MQConnectUtil.getConnection();

        // 创建频道
        Channel channel = connection.createChannel();

        // 一次仅接受一条未经确认的消息
        channel.basicQos(1);

        // 队列声明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        // 定义消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @SneakyThrows
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                String msg = new String(body, StandardCharsets.UTF_8);

                System.out.println("消费者[1]-内容:" + msg);

                Thread.sleep(2 * 1000);

                // 手动回执消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        // 监听队列,将自动应答方式改为false,关闭自动应答机制
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }
}

消费者2

public class Consumer2 {

    public static final String QUEUE_NAME = "test_word_queue";

    public static void main(String[] args) throws Exception {

        // 获取连接
        Connection connection = MQConnectUtil.getConnection();

        // 创建频道
        Channel channel = connection.createChannel();

        // 队列声明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.basicQos(1);

        // 定义消费者
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @SneakyThrows
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
                String msg = new String(body, StandardCharsets.UTF_8);

                System.out.println("消费者[2]-内容:" + msg);

                Thread.sleep(1000);

                // 手动回执消息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };

        // 监听队列,需要将自动应答方式改为false
        boolean autoAck = false;
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }
}

消费结果

那么结果就会像我们之前预想的那样,由于消费者2消费消息花费的时间比消费者1更少,所以消费者2处理的消息的数量要比消费者1处理的消息的数量要多。这里我就不贴图了,大家可以敲代码进行尝试。


今天的文章到这里就结束了,下篇呢,会给介绍介绍另外一种模式,发布订阅模式

日拱一卒,功不唐捐

更多内容请关注:

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 44 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-01-02
个人主页被 2.3k 人浏览