最近打算对自己之前写的个人项目进行一个迭代优化,之前是采用单机单应用的架构进行部署,如果用户量一旦大起来,单机肯定是抗不住的,于是对项目进行这方面的优化
目前项目的架构如下图,用nginx做一个反向代理,一个tomcat,Tomcat和ftp server都部署在同一台机器上面
网站的性能优化主要由下面三个方面进行优化
1. web前端性能优化
2. 服务端性能优化
3. 存储性能优化
下面简单说一下前后端性能优化具体有哪些方法
1.浏览器访问优化
- 减少http请求
- 使用浏览器缓存
- 启用压缩
- 放在页面的最上面,js放在页面的最下面
- 减少cookie的传输
2. CDN加速
3. 反向代理
想要提高并发量,最方便的方法就是直接加机器,也就是构建一个Tomcat集群,具体架构如下
该架构下能很方便的提高并发量,只要简单的配置nginx的负载均衡即可,但是这可能会引发一个问题,我们考虑以下场景
如果当一个用户登陆,请求打到了tomcat server1上的时候,我们把用户信息存储在了Tomcat server1的session中,那么用户访问我们网站的另一个页面时(该页面需要校验登陆),这个请求打到了Tomcat server2上面,会发生什么情况呢?
由于Tomcat 2中并没有存储该用户的登陆信息,我们的网站就会提示用户重新登陆,这样的用户体验会变得非常不好,这个时候我们可以采取一些办法去处理这个问题,办法如下
在不改动代码的前提下,我们只要改变nginx的负载均衡策略,使得该用户每次的请求都打到同一台Tomcat上,这里可以用ip hash的负载均衡策略,nginx会对请求过来的ip做一次hash计算,同一个ip每次都会打到同一台Tomcat上面。
但是,这个方法还是有缺点的,如果用户的ip是动态ip,那么之前场景存在的问题还是没有办法解决,所以我们要引出下一个解决方法——使用redis缓存
redis简介:redis是一款基于k-v形式存储的NoSQL
我们可以通过用redis替换服务端session,把之前需要存储在session中的内容存到redis中,读取的时候,无论有多少台Tomcat集群,都从redis读取,这样就可以很好的解决session失效的问题了,具体架构图如下:
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
/**
* Created by xiao
* User: xiao
* Date: 2018/8/24
* Time: 16:37
*/
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisPool {
//jedis连接池
private static JedisPool pool;
//最大连接数
private static Integer maxTotal = Integer.parseInt("20"));
//最大idle(空闲)状态的jedis实例的个数
private static Integer maxIdle = Integer.parseInt("10");
//最小idle(空闲)状态的jedis实例的个数
private static Integer minIdle = Integer.parseInt("2");
//在borrow一个jedis实例的时候,如果该值为true的时候,实例肯定是OK的
private static Boolean testOnBorrow = true;
//在return一个jedis实例的时候,如果该值为true的时候,放回jedis的连接池的实例肯定是OK的
private static Boolean testOnReturn = true;
//redis ip
private static String redisIp = "127.0.0.1";
//redis port
private static Integer redisPort = 6379;
private static void initPool() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
//连接耗尽时,是否阻塞,false会抛出异常,true会阻塞直到超时,默认是true
config.setBlockWhenExhausted(true);
pool = new JedisPool(config,redisIp,redisPort,1000*2);
}
static{
initPool();
}
public static Jedis getJedis(){
//返回jedis实例
return pool.getResource();
}
public static void returnJedis(Jedis jedis){
if(jedis != null){
jedis.close();
}
}
public static void returnBrokenJedis(Jedis jedis){
if(jedis != null){
jedis.close();
}
}
}
import com.mmall.common.RedisPool;
import com.mmall.common.RedisShardedPool;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ShardedJedis;
/**
* Created by xiao
* User: xiao
* Date: 2018/8/27
* Time: 14:00
*/
/**
* 封装jedis常用api
*/
@Slf4j
public class RedisPoolUtil {
/***
* 设置KV
* @param key
* @return
*/
public static String get(String key){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.get(key);
} catch (Exception e) {
log.error("set key:{} error ",key,e);
RedisPool.returnBrokenJedis(jedis);
return result;
}
RedisPool.returnJedis(jedis);
return result;
}
/***
* 删除key
* @param key
* @return
*/
public static Long del(String key){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.del(key);
} catch (Exception e) {
log.error("del key:{} error ",key,e);
RedisPool.returnBrokenJedis(jedis);
return result;
}
RedisPool.returnJedis(jedis);
return result;
}
/**
* 根据key获取value
* @param key
* @param value
* @return
*/
public static String set(String key,String value){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.set(key,value);
} catch (Exception e) {
log.error("set key:{} value:{} error ",key,value,e);
RedisPool.returnBrokenJedis(jedis);
return result;
}
RedisPool.returnJedis(jedis);
return result;
}
/***
*设置session服务器有效时间
* @param key
* @param value
* @param exTime 单位是秒
* @return
*/
public static String setEx(String key,String value,int exTime ){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.setex(key,exTime,value);
} catch (Exception e) {
log.error("set key:{} exTime {} value:{} error ",key,exTime,value,e);
RedisPool.returnBrokenJedis(jedis);
return result;
}
RedisPool.returnJedis(jedis);
return result;
}
/**
* 设置key的有效期
* @param key
* @param exTime
* @return
*/
public static Long expire(String key,int exTime ){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.expire(key,exTime);
} catch (Exception e) {
log.error("set key:{} exTime {} error ",key,exTime,e);
RedisPool.returnBrokenJedis(jedis);
return result;
}
RedisPool.returnJedis(jedis);
return result;
}
}
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created by xiao
* User: xiao
* Date: 2018/8/27
* Time: 18:31
*/
@Slf4j
public class CookieUtil {
private final static String COOKIE_DOMAIN = ".test.com";
private final static String COOKIE_NAME = "login_token";
public static String readLoginToken(HttpServletRequest request){
Cookie[] cks = request.getCookies();
for (Cookie ck : cks){
log.info("read cookieName:{},cookieValue{}",ck.getName(),ck.getValue());
if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
return ck.getValue();
}
}
return null;
}
/**
*设置浏览器cookie
* @param response
* @param token
*/
public static void writeLoginToken(HttpServletResponse response,String token){
Cookie ck = new Cookie(COOKIE_NAME,token);
//设置cookie的域
ck.setDomain(COOKIE_DOMAIN);
//代表设在根目录
ck.setPath("/");
//防止脚本读取
ck.setHttpOnly(true);
//单位是秒,设置成-1代表永久,如果cookie不设置maxage的话,cookie就不会写入硬盘,写在内存中,只在当前页面有效
ck.setMaxAge(60*60*24*7);
response.addCookie(ck);
}
/**
* 删除浏览器cookie
* @param request
* @param response
*/
public static void delLoginToken(HttpServletRequest request,HttpServletResponse response){
Cookie[] cks = request.getCookies();
for (Cookie ck : cks){
if(StringUtils.equals(ck.getName(),COOKIE_NAME)){
ck.setDomain(COOKIE_DOMAIN);
ck.setPath("/");
ck.setMaxAge(0);
log.info("del cookieName:{},cookieValue{}",ck.getName(),ck.getValue());
response.addCookie(ck);
return;
}
}
}
}
我们在项目中,如果要把对象存到redis,就要把对象序列化成字符串,然后存到redis中去,如果需要使用对象,就从redis中取出字符串,反序列化成对象
/**
* Created by xiao
* User: xiao
* Date: 2018/8/27
* Time: 14:39
*/
import com.mmall.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.codehaus.jackson.map.DeserializationConfig;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.type.JavaType;
import org.codehaus.jackson.type.TypeReference;
import java.text.SimpleDateFormat;
@Slf4j
public class JsonUtil {
private static ObjectMapper objectMapper = new ObjectMapper();
static {
//设置序列化属性
//对象全部字段都列入
objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.ALWAYS);
//取消默认把日期转换成timestamp形式
objectMapper.configure(SerializationConfig.Feature.WRITE_DATE_KEYS_AS_TIMESTAMPS,false);
//忽略空bean转json的错误
objectMapper.configure(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS,false);
//统一日期格式
objectMapper.setDateFormat(new SimpleDateFormat(DateTimeUtil.STANDARD_FORMAT));
//反序列化属性
//忽略在json字符串中存在,但是java对象中不存在对应属性的情况,防止错误
objectMapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES,false);
}
public static String obj2String(T obj){
if(obj == null){
return null;
}
try {
return obj instanceof String ? (String)obj : objectMapper.writeValueAsString(obj);
} catch (Exception e) {
log.warn("Parse object to string error ",e);
return null;
}
}
/**
* 返回漂亮的序列化的字符串
* @param obj
* @param
* @return
*/
public static String obj2StringPretty(T obj){
if(obj == null){
return null;
}
try {
return obj instanceof String ? (String)obj : objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(obj);
} catch (Exception e) {
log.warn("Parse object to string error ",e);
return null;
}
}
public static T string2Obj(String str,Class clazz){
if(StringUtils.isEmpty(str)||clazz == null){
return null;
}
try {
return clazz.equals(String.class) ? (T)str : objectMapper.readValue(str,clazz);
} catch (Exception e) {
log.warn("Parse string to object error ",e);
return null;
}
}
public static T string2Obj(String str, TypeReference typeReference){
if(StringUtils.isEmpty(str)||typeReference == null){
return null;
}
try {
return (T)(typeReference.getType().equals(String.class)? str : objectMapper.readValue(str,typeReference)); } catch (Exception e) {
log.warn("Parse string to object error ",e);
return null;
}
}
public static T string2Obj(String str,Class> collectionClass,Class>... elementClasses){
JavaType javaType = objectMapper.getTypeFactory().constructParametricType(collectionClass,elementClasses);
try {
return objectMapper.readValue(str,javaType);
}catch (Exception e){
log.warn("Parse string to object error ",e);
return null;
}
}
public static void main(String[] args) {
User u1 = new User();
u1.setId(1);
u1.setEmail("[email protected]");
String userJson = JsonUtil.obj2String(u1);
String userJsonPretty = JsonUtil.obj2StringPretty(u1);
log.info("userJson:{}",userJson);
log.info("userJsonPretty:{}",userJsonPretty);
User user = JsonUtil.string2Obj(userJson,User.class);
User user2 = JsonUtil.string2Obj(userJsonPretty,User.class);
}
}
/**
* 用户登录
* @param username
* @param userpwd
* @param session
* @return
*/
@RequestMapping(value = "login.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse login(String username, String userpwd, HttpSession session, HttpServletResponse httpServletResponse)
{
ServerResponse response = iUserService.login(username,userpwd);
if(response.isSuccess()){
//session.setAttribute(Const.CURRENT_USER,response.getData());
//把session存储改成cookie记录session id,然后通过在指定域种下cookie,下次访问要验证登陆的话,直接读取session id 去redis取数据,以此判断登陆
CookieUtil.writeLoginToken(httpServletResponse,session.getId());
RedisPoolUtil.setEx(session.getId(), JsonUtil.obj2String(response.getData()),Const.RedisCacheExtime.REDIS_SESSION_EXTIME);
}
return response;
}
@RequestMapping(value = "get_information.do",method = RequestMethod.POST)
@ResponseBody
public ServerResponse get_information(HttpSession session,HttpServletRequest httpServletRequest){
//从cookie中获取session id(token)
String loginToken = CookieUtil.readLoginToken(httpServletRequest);
if(StringUtils.isEmpty(loginToken)){
return ServerResponse.createByErrorCodeMessage( ResponseCode.NEED_LOGIN.getCode(),"未登录,需要强制登录status=10");
}
String jsonStr = RedisPoolUtil.get(loginToken);
//json反序列化成对象的方法
User user = JsonUtil.string2Obj(jsonStr,User.class);
if(user == null || user.getId()==null){
return ServerResponse.createByErrorCodeMessage( ResponseCode.NEED_LOGIN.getCode(),"未登录,需要强制登录status=10");
}
return iUserService.getInformation(user.getId());
}
这次优化解决了集群session失效的问题,也进一步的优化了项目代码,学习到了如何使用redis实现单点登录,不过这个架构还是有可以优化的地方,比如mysql优化,redis集群化,一步一步去迭代项目,提升自己的编码与思考能力。