微服务设计指导-redis双缓存设计解决一个app版本检查API结果导致了系统崩溃

微服务设计指导-redis双缓存设计解决一个app版本检查API结果导致了系统崩溃,第1张

微服务设计指导-redis双缓存设计解决一个app版本检查API结果导致了系统崩溃 背景

这个问题其实我在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 Mono versionCheckDoubleBuffer() {
		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);
	}
}
 VersionCheckService.java
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



	
		

	

	
		SELECT version_id
		FROM app_version order by created_date desc limit 1
	

走双缓存和仅走Redis的比较

我使用了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、应该很快的!什么叫应该的?在互联网领域一切拍脑袋最后都会被“自我打脸”;

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5715620.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存