snowflake算法学习 作者: nbboy 时间: 2020-08-03 分类: 默认分类 评论 ###起源 内部IM项目需要标识不同消息的唯一性,我设计了一个ID值用来作为标识,关键在于这个值如何生成。该ID值需要满足以下需求: - 在高并发的时候也要保证多机唯一性 - 生成速度足够快,因为我们是IM系统,对实时性有需求 - 最好是数字的,这样排序可能更快一些 ###方案 查了下资料,Twitter 开源出一种ID生成算法,叫snowflake算法。学习了一下,其实也没什么新鲜东西,关键点也是可以想到的,就是时间戳+序列进行唯一性的保证。 SnowFlake算法是Twitter设计的一个可以在分布式系统中生成唯一的ID的算法,它可以满足Twitter每秒上万条消息ID分配的请求,这些消息ID是唯一的且有大致的递增顺序。 SnowFlake的算法是产生一个int64整型的ID,这段ID一共分为4部分,通过标示部分、时间戳、worker的Id、生成ID的序号来组合成为一个自增的ID,并且是全局唯一的。 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000  1. 1位标识部分 由于最高位是符号位,正数是0,负数是1,所以一般为0 1. 41位时间戳部分,这个是毫秒级别的的时间,但是使用的是当前时间减去系统开始时的差值,因为如果使用现在的值,会减少生成ID的唯一性, 1. 10位节点部分,前5位作为数据中心的标示,后5位作为机器标示,可以部分1024个节点 1. 12位序号,支持同一毫秒内同一个节点可以生成4096个ID; ###坑 网上也说了,这个算法就像其他基于time的算法一样,都不可避免的有一个漏洞,就是时间回拨问题,这个如何保证?其实在我这个项目中,因为简单原则,我们IM部署机器也不多,就几台,发号只需要集中在1,2台进行发号就可以。对于1,2台还是可以通过人工周期性同步或者脚本检测的方法来预防的。 最后,源码我放在GITHUB上了[Snowflake](https://github.com/x-debug/xfsnowflake.git "Snowflake"),欢迎读者来喷我!
从线上宕机引起的问题谈开去 作者: nbboy 时间: 2020-06-18 分类: 默认分类,PHP 评论 ### 背景 公司的外网线上项目连续宕机将近2周时间,后来通过查找日志,发现是某度蜘蛛大量爬取造成。因为公司花钱进行了SEO推广,所以也不能直接禁掉蜘蛛,所以一度造成网站打不开的情况。期间增加带宽,扩容cpu,增强搜索引擎集群等方案,都得不到好的效果。 ### 分析 开始优化代码,记录慢日志,在云平台看慢查询,渐渐优化慢查询,但是问题依旧。慢慢通过分析慢日志注意到如下细节:  这里我摘取了两个日志片段,其实类似日志片段还有非常多。可以看到将近相同的时间不同的许多进程都进行了GC,而这里的所谓GC是什么? 在StartSession.php上看到了如下代码: ```php /** * Remove the garbage from the session if necessary. * * @param \Illuminate\Session\SessionInterface $session * @return void */ protected function collectGarbage(SessionInterface $session) { $config = $this->manager->getSessionConfig(); // Here we will see if this request hits the garbage collection lottery by hitting // the odds needed to perform garbage collection on any given request. If we do // hit it, we'll call this handler to let it delete all the expired sessions. if ($this->configHitsLottery($config)) { $session->getHandler()->gc($this->getSessionLifetimeInSeconds()); } } ``` 这里注释的意思是有一定概率进行GC,并不是每次都进行GC,先来看下GC做了什么事情。 因为配置的是文件session,所以我们找到文件Session的实现文件FileSessionHandler.php,摘取GC函数代码如下: ```php /** * {@inheritdoc} */ public function gc($lifetime) { $files = Finder::create() ->in($this->path) ->files() ->ignoreDotFiles(true) ->date('<= now - '.$lifetime.' seconds'); foreach ($files as $file) { $this->files->delete($file->getRealPath()); } } ``` 代码的意思很简单,就是通过搜索文件,获取到过期的session文件然后进行删除,这里的过期时间在session.php的配置里定义: ```php /* |-------------------------------------------------------------------------- | Session Lifetime |-------------------------------------------------------------------------- | | Here you may specify the number of minutes that you wish the session | to be allowed to remain idle before it expires. If you want them | to immediately expire on the browser closing, set that option. | */ 'lifetime' => 120, 'expire_on_close' => true, ``` 可以看到默认是2个小时,下面一个参数的意义是是否要在浏览器关闭的时候立即过期!刚才说到了,有一定概率执行这个GC,那这个概率多大呢? ```php /** * Determine if the configuration odds hit the lottery. * * @param array $config * @return bool */ protected function configHitsLottery(array $config) { return random_int(1, 100) <= 2; } ``` 上面代码是我把默认配置替换后的结果,也就是会有2%的可能会触发一次GC!而一次GC我们要回收两个小时之前的过期session文件。如果在并发量稍微有点大的情况下,要删除的文件其实是非常可观的,删除这个IO操作就是直接导致了cpu负荷直接超载的罪魁祸首。 ### 解决方案 方法很简单,就是换driver,比如集中式的redis session方案。 ### 总结 虽然这次项目的主要负责人不是我,不过通过这次抓取日志,跟踪代码,分析代码学到了很多优化的一手资料。Laravel其实是一个相当灵活的框架,文件session方式只能在测试环境进行应用,而在线上环境必须上集中式session方式,不然流量起来的时候,就会吃苦头。Laravel如不经过优化的情况下,他的性能是不高的,好在他也提供了很多可以优化的扩展接口,算的上是目前PHP里相当成熟的框架了。
SpringBoot集成FreeMarker 作者: nbboy 时间: 2020-04-02 分类: 默认分类,Java 评论 ## SpringBoot 集成Freemarker Freemarker是一个Java模板系统,集成非常简单。 1. 第一步在POM文件中引入自动配置包 ``` org.springframework.boot spring-boot-starter-freemarker ``` 2. 第二部在模板目录下创建模板test.ftlh,内容为 ```html ${msg} ``` 3. 第三部创建调用的Controller ```java @Controller @RequestMapping("/auth") public class AuthController { @RequestMapping("/test") public String test(Model model){ model.addAttribute("msg", "Hello,Freemarker"); return "auth/test"; } } ``` 4.运行,就能看到动态内容已经渲染出来了。 ## Freemarker API简单使用 ## SpringMVC调用Freemarker浅析 完整的MVC调用链这里先隐去,从org.springframework.web.servlet.DispatcherServlet#doDispatch看下调用的逻辑: ```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } //这里的开始处理控制器返回的内容,渲染视图就在这里面处理 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } } ``` 在org.springframework.web.servlet.DispatcherServlet#processDispatchResult里面,我们看到了渲染的方法。 ```java private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; //省略部分代码 // Did the handler return a view to render? if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } //省略部分代码 } ``` 再看下这个方法org.springframework.web.servlet.DispatcherServlet#render,完整的查找模板解析器和解析器渲染的过程就在这里。 ```java protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { // Determine locale for request and apply it to the response. //省略部分代码 View view; //获得视图名称 String viewName = mv.getViewName(); if (viewName != null) { // We need to resolve the view name. //这里查找到模板解析器 view = resolveViewName(viewName, mv.getModelInternal(), locale, request); } try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } //做真正的渲染 view.render(mv.getModelInternal(), request, response); } //省略部分代码 } ``` OK,我们看下org.springframework.web.servlet.DispatcherServlet#resolveViewName的方法是怎样的。 ```java protected View resolveViewName(String viewName, @Nullable Map model, Locale locale, HttpServletRequest request) throws Exception { if (this.viewResolvers != null) { for (ViewResolver viewResolver : this.viewResolvers) { View view = viewResolver.resolveViewName(viewName, locale); if (view != null) { return view; } } } return null; } ``` 我们的Freemarker解析器FreeMarkerViewResolver也在里面,这个解析器FreeMarkerViewResolver实现了ViewResolver接口就可以了。 至于渲染,刚才也看到会调用view的render方法,然后通过层层调用,最后调用org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender方法,方法如下所述: ```java protected void doRender(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose model to JSP tags (as request attributes). exposeModelAsRequestAttributes(model, request); // Expose all standard FreeMarker hash models. SimpleHash fmModel = buildTemplateModel(model, request, response); // Grab the locale-specific version of the template. Locale locale = RequestContextUtils.getLocale(request); processTemplate(getTemplate(locale), fmModel, response); } protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response) throws IOException, TemplateException { template.process(model, response.getWriter()); } ``` 至此,MVC里的调用就结束了,Template是Freemarker包下的模板对象。也可看到最后带过去的是数据模型和输出流,有了这两个东西,要动态输出是轻而易举的事情了。 ## SpringBoot Freemarker 自动配置如何实现 配置Freemarker主要是通过org.springframework.boot.autoconfigure.freemarker.FreeMarkerProperties实现的,不过有一些属性是来自基类org.springframework.boot.autoconfigure.template.AbstractViewResolverProperties,其中属性还是挺多,主要常用的比如 ```java /** * Whether to enable MVC view resolution for this technology. */ private boolean enabled = true; /** * Whether to enable template caching. */ private boolean cache; /** * Content-Type value. */ private MimeType contentType = DEFAULT_CONTENT_TYPE; /** * Template encoding. */ private Charset charset = DEFAULT_CHARSET; /** * Prefix that gets prepended to view names when building a URL. */ private String prefix; /** * Suffix that gets appended to view names when building a URL. */ private String suffix; ``` ## 总结 虽然现在都提倡前后端分离,不过适当了解了解模板渲染有关的内容也是挺有帮助的,因为就目前来说毕竟用SPA一体化富应用在SEO方面做得不够理想,还有对于比较简单不怎么交互的页面也完全可以用后端渲染完成,所以在这方面花适当的时间还是值得的。