这个问题其实我在2016年碰到过。APP或者是小程序都有一个版本检查以便于前端进行APP的强制更新。
也适合在后台和to c端前台匹配个人信息安全所需的用户协议、消息推送协议版本进行校验用。
它只是一个API,这个API会在APP或者是小程序的入口入先于首页和to c端进行交互。
很多人觉得这就是一个versionCheck的get请求,返回一个版本号,然后在手机端比对一下版本号,就这么简单,要什么设计呀(典型的产品经理思维)。
简单?
在大促时或者是抢券、领红包场景时,500并发一来,直接整个首页打开时就是白屏。然后用APM工具看了一下,原来 只是因为有一根请求抖动了一下,结果导致了雪崩。
于是,有所谓产品经理、架构师又来设计了。。。唉呀。。。这个API每次走DB,简单,我们现在改走Redis吧。
我还没有来得及说:别介。。。
结果他们做好了、上线了。
于是第二天又是抢券、领红包,这次改完后系统到时顶住了5分钟,第6分钟白屏又来了。然后我们打开APM一看。由于前端的吞吐量打开了,因此外部的请求进一步汹涌进来。最终把Redis连接打爆了,我们看了一下Redis的连接设了10万,好家伙。
于是,一群人傻了,就不知道该怎么办了。
提出解决方案其实这个问题很简单,因为以上无论是原来的传统设计还是后来改成了走Redis的设计,它都无法响应万级、十万级乃至百万级别的并发。
这种问题就需要使用云原生法则去解决。听听好高大上哈?什么叫云原生?让我们接地气、说“人话”。
云原生有好几个点,其实有一个很重要的点,那就是处理上述这类问题:随着流量不断的增加,你的应用服务也是可以“d性”扩容的,但是这由于上述设计d性扩容面临着以下这么一个问题那就是你的云服务器使用K8S不断的d幅本(集群个数)而你可以使用的“资源”连接却受制于DB、Redis的最大连接数,这是一个很经典的悖论。此时这个应用就不符合“云原生”了。
因此我们要彻底把“中间件”的资源连接限制问题去除掉,因此我们使用了“双缓存”设计机制,如下:
即最后一种设计,它就是在你的Redis层前再挡一层jvm本地内存。
它在取数据时其实就是下面这段逻辑:
- 请求进来先在本地内存里找是否有保存的值;如果本地内存里找不到再找Redis;如果在Redis里再找不到的话就去Db里找;找到后把它放入本地缓存;
这种设计就可以做到随着外部请求不断的扩大、企业只需要单纯的扩大幅本数去顶住并发请求带来的连接压力即可。
它把原先外部请求的连接数对中件间、DB的连接冲击转换成了固定的应用服务器(app1-x)个连接数,成指数级别的削减了系统资源的开销的同时通过不断d性扩容应用服务器来支持外部汹涌而来的http并发请求数。
核心代码 VersionController.java为了做演示和比较我在controller里做了两个api:
versionCheckDoubleBufferversionCheckRedis
package org.mk.demo.doublebuffer.controller; import javax.annotation.Resource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.mk.demo.doublebuffer.service.VersionCheckService; import org.mk.demo.doublebuffer.vo.AppVersionCheckBean; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; import org.mk.demo.util.response.ResponseBean; import org.mk.demo.util.response.ResponseCodeEnum; @RestController public class VersionController { private Logger logger = LogManager.getLogger(this.getClass()); @Resource private VersionCheckService versionCheckService; @GetMapping("/versionCheckDoubleBuffer") public MonoVersionCheckService.javaversionCheckDoubleBuffer() { ResponseBean responseBean = new ResponseBean(); try { String versionId = versionCheckService.getLatestVersionFromDoubleBuffer(); logger.info(">>>>>>versonId->{}", versionId); responseBean = new ResponseBean(ResponseCodeEnum.SUCCESS.getCode(), "success", versionId); } catch (Exception e) { responseBean = new ResponseBean(ResponseCodeEnum.FAIL.getCode(), "system error"); logger.error(">>>>>>versionCheckFromDoubleBuffer error: {}", e.getMessage(), e); } return Mono.just(responseBean); } @GetMapping("/versionCheckRedis") public Mono versionCheckRedis() { ResponseBean responseBean = new ResponseBean(); try { String versionId = versionCheckService.getLatestVersionFromRedis(); logger.info(">>>>>>versonId->{}", versionId); responseBean = new ResponseBean(ResponseCodeEnum.SUCCESS.getCode(), "success", versionId); } catch (Exception e) { responseBean = new ResponseBean(ResponseCodeEnum.FAIL.getCode(), "system error"); logger.error(">>>>>>versionCheckFromRedis error: {}", e.getMessage(), e); } return Mono.just(responseBean); } }
package org.mk.demo.doublebuffer.service; import java.util.concurrent.TimeUnit; import javax.annotation.Resource; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.mk.demo.doublebuffer.cache.LocalCache; import org.mk.demo.doublebuffer.dao.AppVersionCheckDao; import org.mk.demo.doublebuffer.vo.AppVersionCheckBean; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import com.alibaba.druid.util.StringUtils; @Service public class VersionCheckService { protected Logger logger = LogManager.getLogger(this.getClass()); private final static String VERSION_ID_REDIS = "redis-versionId"; @Resource private RedisTemplate redisTemplate; @Resource private AppVersionCheckDao appVersionCheckDao; public String getLatestVersionFromRedis() throws Exception { String versionId = ""; try { versionId = (String) redisTemplate.opsForValue().get(VERSION_ID_REDIS); if (StringUtils.isEmpty(versionId)) {// 如果redis里为空走db logger.info(">>>>>>redis里也找不到开始走db查找"); versionId = appVersionCheckDao.getLatestVersionId(); redisTemplate.opsForValue().set(VERSION_ID_REDIS, versionId, 120, TimeUnit.SECONDS);// db拿出来后塞入Redis } } catch (Exception e) { logger.error(">>>>>>getLatestVersionFromRedis error {}", e.getMessage(), e); throw new Exception(">>>>>>getLatestVersionFromRedis error :" + e.getMessage(), e); } return versionId; } public String getLatestVersionFromDoubleBuffer() throws Exception { String versionId = ""; try { versionId = getVersion(); } catch (Exception e) { logger.error(">>>>>>getLatestVersionFromDoubleBuffer error {}", e.getMessage(), e); throw new Exception(">>>>>>getLatestVersionFromDoubleBuffer error :" + e.getMessage(), e); } return versionId; } private String getVersion() throws Exception { String versionId = ""; try { // 先从本地内存找 versionId = LocalCache.getVersionId("versionId"); if (StringUtils.isEmpty(versionId)) {// 如果本地缓存为空走Redis logger.info(">>>>>>本地内存找不到开始走redis查找"); versionId = (String) redisTemplate.opsForValue().get(VERSION_ID_REDIS); if (StringUtils.isEmpty(versionId)) {// 如果redis里为空走db logger.info(">>>>>>redis里也找不到开始走db查找"); versionId = appVersionCheckDao.getLatestVersionId(); redisTemplate.opsForValue().set(VERSION_ID_REDIS, versionId, 120, TimeUnit.SECONDS);// db拿出来后塞入Redis } LocalCache.setVersionId("versionId",versionId);// 如果是从redis或者是从db拿,拿到后都要塞回本地内存中去 } else { logger.info(">>>>>>李地内存找到了直接就返回了"); } } catch (Exception e) { logger.error(">>>>>>getVersion error {}", e.getMessage(), e); throw new Exception(">>>>>>getVersion error :" + e.getMessage(), e); } return versionId; } }AppVersionCheckDao.java
package org.mk.demo.doublebuffer.dao; import org.mk.demo.doublebuffer.vo.AppVersionCheckBean; import org.springframework.stereotype.Repository; @Repository public interface AppVersionCheckDao { public String getLatestVersionId(); }AppVersionCheckDao.xml
走双缓存和仅走Redis的比较SELECT version_id FROM app_version order by created_date desc limit 1
我使用了1,000个线程分别对:versionIdFromRedis和versionIdFromDoubleBuffer进行了压测,得到的结果如下:
其实走纯Redis这块压测当我把线程数加到了10,000万,我的Redis被打爆了。
而走双缓存这块即使我把压测的线程增加到了10万个并发(我动用了200个jmeter client),它的这个average response还是在30-40毫秒内。
截图中的error rate大家可以忽略,是因为一个jmeter client我用了500个线程,这对于jmeter所在的OS来说太高了,jmeter client端自身因为http进程堆积从jmeter client处打断了少许和应用服务器间的连接以释放 *** 作系统的tcp连接,因此可以直接忽略。
这个实验足以说明了双缓存带来的好处。
总结双缓存:代码上略显复杂,但是它是符合云原生的,它把原先to c端的请求受制于redis、db的最大连接数变成了可几乎无限、不受限制、只要你使用云和k8s理论上可以无限接受to c端的http并发请求数;如果只使用redis来缓存、加速to c端请求最终会因为redis本身被无限的to c端进入的请求打爆;如果使用传统设计,觉得这只是一个小玩意、只是一个版本check、应该很快的!什么叫应该的?在互联网领域一切拍脑袋最后都会被“自我打脸”;
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)