精华内容
下载资源
问答
  • OA多级审批流程表设计方案以及开发思路(非常细节)

    万次阅读 多人点赞 2020-07-27 15:39:30
    OA审批工作流设计解决方案

    OA(office automation) 想必大家都已不陌生,甚至还非常熟悉,是的没错,本文就来讲解一下OA中的核心业务,审批流程是如何一步步实现的。


    本文干货满满,建议静下心来细细品

    被审核流程

    首先填写好表单相关信息,然后点击审批人,从公司部门树中点击相应部门,加载部门相关角色用户,最后再指定审批人
    部门树
    值得吹嘘的一点是这里的审批人可供用户自行动态选择,并且审批层级也是随着审批人的数量动态增减

    以加班表单为例子
    加班审批
    指定完成之后,点击提交即可。
    然后再由相应的审批人逐级进行审批,当其中有一个不通过,则整个流程不
    通过,当所有的审批人全部通过才可通过


    OK流程已经清楚了,接下来我们来进行表结构的设计
    只需要两张核心的审批表即可,其他需要进行审批流的业务表通过审批流编号FlowNo 关联这两张核心业务表,我们来看一下

    审批流主表 AuditFlow

    Column Name Data Type Describe
    FlowNo Varchar(50) not null primary key 审批编号返回yyyMMddHHmm+n位随机数
    Title Nvarchar(50) not null 标题(例如:某某人的加班申请)
    BusType Varchar(20) not null 审批类型(根据业务表定义Code,区分表单)
    AddUserNo Datetime not null 申请人
    AddTime Varchar(50) not null 添加时间
    ApproStatus Int not null 审核状态(1.待审,2.通过.3.驳回,4.撤销)

    这两张表的关系是一对多,明细表的数量取决与表单提交添加的审核人数量
    ApproFlow:1 =======> n :ApproFlowDetail

    审批流明细表 AuditFlowDetail

    Column Name Data Type Describe
    ID Int not null primary key identity(1,1) 主键自增列
    FlowNo Varchar(50) not null 审批编号,关联主表
    AuditUserNo Varchar(50) not null 审核人
    AuditRemark Nvarchar(500) 审核备注
    AuditTime Datetime 审核时间
    AuditStatus Int not null 审核状态(1.审核中,2.待我审批.3.通过,4.驳回)

    如此一来,OA审批流程的两张核心业务表就设计完成了.
    那么问题来了,其他相关表呢?
    别急,慢慢来嘛。首先用户表肯定是需要的,因为表单申请人和审核人都是关联的用户No,因为用户是根据部门走的,那么还需要设计一张部门表,再设计一张用户和部门相关联的表,把用户和部门联系起来,就可以从部门中选取相应角色。当然简单点的逻辑可以不用部门,直接搜索全部用户,这里就不再设计了哈。

    有了用户表和审批业务核心表,接下来就可以根据公司业务需求,来设计相关的审批流程业务表了,这里就拿加班申请来举个例子,当用户需要进行加班的时候,肯定是需要走审批流程的,那么再来设计一张加班申请表

    加班表 OverTimeAsk

    Column Name Data Type Describe
    FlowNo Varchar(50) not null primary key 关联AuditFlow表
    AddUserNo Varchar(20) not null 添加用户No
    AddTime Datetime not null 添加时间
    AskReason Nvarchar(50) not null 加班事由
    Remark Nvarchar(100) 备注
    LeaveTimeFrom Datetime not null 加班开始时间
    LeaveTimeTo Datetime not null 加班结束时间
    OverTimeHours decimal(10,2) 加班总小时

    此时,再回到文章一开始的流程,脑海中的思路是不是就慢慢的浮现出来了呢,嘻嘻

    库设计好了,剩下的就是由程序实现完成了,那么问题又来了,如何去实现呢?借助问题,来进行驱动成长,下面就来探讨具体如何实现.

    有了以上设计的表做铺垫,就可以为所欲为啦!
    填写完加班申请表单,选择部门相关负责审批人
    如主管,部门经理,总经理,此时进行表单提交

    提交需要进行的操作

    • 录入当前审批业务表,也就是加班申请表的数据
    • 审批流主表插入一条数据
    • 审批流明细表插入三条数据
    • 对添加的第一个审核人发送相关通知消息

    注意要点:
    1.以上三条是同时进行操作,必须要满足事务,否则数据会出现问题
    2.三条数据插入的FlowNo字段必须是相同的
    3.插入审批流主表数据的时候,BusType字段的值可以设置为OverTimeAsk,审核状态默认1(待审核)
    4.插入审批流明细表数据的条数取决与用户提交表单选择的审核人数量,如这里选择了三个审批人,就需要插入三条数据,第一条的审核状态 设为 2(待我审批),其他两条的审核状态设为1(审核中)
    5.插入加班申请表对月份进行判定,不允许跨月加班

    表单提交的操作完成了,下面就开始论到审核操作的流程了
    首先,要有一个待我审批的入口,查询出所有待我审核的表单

    • 将AuditFlow表和AuditFlowDetail表通过FlowNo关联查询

    • 过滤AuditFlow表审核状态为1并且AuditFlowDetail表审核状态为2的数据

       也可以根据AuditFlow表的BusType字段进行审批表单的分类
      

    审核操作,基本上分为审核通过和不通过, 当然也可以根据业务自行扩展其他的审核操作。

    实现思路与细节如下:

    1. 根据表单提交操作来判断审核是否同意
    2. 根据FlowNo和AuditUserNo以及AuditStatus为2(待我审批) 的条件去查询AuditFlowDetail表,如果数据为空,则此单已进行过审核操作,直接返回.
    3. 如果上一条查询的数据不为空,则可以将当前审核明细单数据的审核状态设置为通过or驳回
    4. 如果当前审核明细单的待审核数量大于一,则说明还需要向下一级传递审核,同时将下一级数据的审核状态设置为待我审核,并发送相关通知
    5. 如果当前审核明细单数据全部为审核通过,则将AuditFlow表的审核状态设为通过
    6. 如果当前审核明细单有一条审核不通过,则将AuditFlow表的审核状态设为不通过

    实现细节
    如果审核同意则,根据FlowNo查询出所有AuditFlowDetail表数据,然后进行过滤,分别统计审核通过和审核不通过的数据条数,并记录第一个审核状态为审核中的数据.此时可根据条件执行上面的第4,5,6条
    审核不同意操作大致同上

    OK,整个多级审批流程就实现完啦,如果你仔细看完此文,相信你一定会茅塞顿开

    当然OA的审批业务远远不止这么一点,还有其他的表单审批,比如工作汇报审批表,还可增加关联的附件表,提交工作内容的同时上传相关文件或者照片存放在服务中,方便审核人随时在线预览或者下载到本地
    还可根据业务需求自行扩展相关表单
    OA表单
    以上所有表单的审批流程都是围绕基于两张核心业务表来实现.

    如果有其他问题,欢迎留言咨询,不定期更新

    展开全文
  • java 项目日志管理设计方案

    万次阅读 热门讨论 2017-05-27 16:00:00
    java 项目日志管理设计方案 因项目需要记录整个系统的操作记录,考虑到系统操作日志的数据量,单表很容易达到瓶颈,导致查询效率低下,顾使用分表方案,减小数据库的负担,缩短查询时间。目前对于分表的解决方案有...

    java 项目日志管理设计方案

    因项目需要记录整个系统的操作记录,考虑到系统操作日志的数据量,单表很容易达到瓶颈,导致查询效率低下,顾使用分表方案,减小数据库的负担,缩短查询时间。目前对于分表的解决方案有很多,本博文主要讲解博主自行实现的日志管理的解决方案,如有遗漏或错误的请各位大佬多多包涵
    鉴于总是有人私信要demo,这里将以前搭的一个简易的项目贴出来:https://gitee.com/jiangliuhong/syslog.git


    1 创建日志表

    1.1 日志表Sql语句如下

    具体表设计随项目情况而变化

    表创建SQL语句

    CREATE TABLE `sys_user_log` (
      `log_id` varchar(32) NOT NULL COMMENT '日志表id,uuid',
      `user_id` varchar(32) DEFAULT NULL COMMENT '用户id,记录操作用户',
      `module_name` varchar(225) NOT NULL COMMENT '模块名称',
      `operate` varchar(225) NOT NULL COMMENT '操作名称',
      `time` datetime NOT NULL COMMENT '操作时间',
      `class_name` varchar(225) NOT NULL COMMENT '类名称',
      `method_name` varchar(225) NOT NULL COMMENT '方法名称',
      `params` longtext COMMENT '传入参数',
      `ip` varchar(225) NOT NULL COMMENT '操作ip',
      PRIMARY KEY (`log_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    

    1.2 日志表创建

    因考虑到项目情况,顾为每月创建一个日志表

    1.2.1 表创建方法

    创建service、dao方法

    DbMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.carfi.vrcp.dao.SysDbMapper">
        <update id="createUserLogTable" parameterType="java.lang.String">
        	create table #{name,jdbcType=VARCHAR} like sys_user_log
        </update>
    </mapper>
    
    

    DbMapper.java

    public interface SysDbMapper {
        /**
         *  创建数据表
         * @param name 表名
         */
        void createUserLogTable(String name);
    }
    

    DbService.java

    public interface SysDbService {
        /**
         * 创建数据表
         *
         * @param preName   名称前缀
         * @param isSufTime 是否显示 时间后缀
         */
        void createTable(String preName, Boolean isSufTime);
    }
    
    

    DbServiceImpl.java

    
    @Service("dbService")
    public class DbServiceimpl implements DbService {
    
        @Autowired
        private DbMapper dbMapper;
    
        @Override
        public void createTable(String preName, Boolean sufTime) {
            if (sufTime) {
                dbMapper.createTable(preName + "_" + getSufName());
            } else {
                dbMapper.createTable(preName);
            }
        }
        
        /**
         * 获取名称后缀<br>
         * 格式: 20170301
         *
         * @return
         */
        private String getSufName() {
            Date d = new Date();
            String s = DateUtil.date2String(d, false);
            return s.replace("-", "");
        }
    }
    
    

    1.2.2 创建定时任务(每月第一天凌晨执行)

    BaseQuartz.java

    
    /**
     * 报警定时任务接口
     * 
     */
    public interface BaseQuartz {
    
    	/**
    	 * 定时任务执行方法
    	 */
    	public void work();
    }
    
    

    DbQuartz.java

    
    public class DbQuartz implements BaseQuartz {
        
        private static final Logger log = LoggerFactory.getLogger(DbQuartz.class);
        /**
         * 默认表名
         */
        private static String tableName = "sys_user_log";
    
        @Override
        public void work() {
            try {
                //创建表
                DbService dbService = (SysDbService) SpringUtil.getBean("sysDbService");
                dbService.createTable(tableName, true);
                log.info("创建表" + tableName + "成功");
                //更新系统用户日志表表缓存
                SysCacheUtil.flushSysUserLogTableName();
            } catch (Exception e) {
                log.error("创建表" + tableName + "失败");
                work();
            }
        }
        
    }
    
    

    quartz配置文件:spring-quartz.xml

    
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
    	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:cache="http://www.springframework.org/schema/cache"
    	xsi:schemaLocation="  
           http://www.springframework.org/schema/context  
           http://www.springframework.org/schema/context/spring-context.xsd  
           http://www.springframework.org/schema/beans  
           http://www.springframework.org/schema/beans/spring-beans.xsd  
           http://www.springframework.org/schema/tx  
           http://www.springframework.org/schema/tx/spring-tx.xsd  
           http://www.springframework.org/schema/aop  
           http://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/cache  
           http://www.springframework.org/schema/cache/spring-cache-3.2.xsd
         ">
         
         <!-- 工作的bean -->
    	<!-- 数据库定时任务 开始 -->
    	<bean id="dbQuartz" class="com.carfi.vrcp.quartz.DbQuartz" />
    	<bean id="dbQuartzJobDetail"
    		class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
    		<property name="targetObject">
    			<ref bean="dbQuartz" />
    		</property>
    		<property name="targetMethod">
    			<value>work</value>
    		</property>
    	</bean>
    	<!-- 定义触发器 -->
    	<bean id="dbQuartzJobTrigger"
    		class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
    		<property name="jobDetail">
    			<ref bean="dbQuartzJobDetail" />
    		</property>
    		<property name="cronExpression">
    			<!-- 每月第一天凌晨执行 -->
    			<value>0 0 0 1 * ?</value>
    		</property>
    	</bean>
    	
    	<!-- 启动触发器的配置开始 -->
    	<bean name="startQuertz" lazy-init="false" autowire="no"
    		class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    		<property name="triggers">
    			<list>
    				<ref bean="dbQuartzJobTrigger" />
    			</list>
    		</property>
    	</bean>
    	
    </beans>  
    
    

    到止为止日志表自动创建逻辑完成

    2 日志表数据插入查询

    因考虑查询速度,采用根据传入的时间进行联合查询,规定只能查询连续3个月的日志数据,即前台传入 20170301,20170525 则联合表sys_user_log20170301,sys_user_log20170401,sys_user_log20170501三表进行联合查询。

    2.1 日志相关类源代码

    主要代码包括,日志实体类,日志查询类,日志表相关的dao、service 类

    SysUserLog.java

    
    import java.io.Serializable;
    import java.util.Date;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    /**
     * 系统用户日志
     */
    public class SysUserLog implements Serializable {
    
    	/**
    	 * 
    	 */
    	private static final long serialVersionUID = 1L;
    
    	/**日志id*/
    	private String logId;
    	/**用户id*/
    	private String userId;
    	/**模块名称*/
    	private String moduleName;
    	/**操作*/
    	private String operate;
    	/**时间*/
    	private Date time;
    	/**类名*/
    	private String className;
    	/**方法名*/
    	private String methodName;
    	/**传入参数*/
    	private String params;
    	/**操作ip*/
    	private String ip;
    	
    	public String getLogId() {
    		return logId;
    	}
    	public void setLogId(String logId) {
    		this.logId = logId;
    	}
    	public String getUserId() {
    		return userId;
    	}
    	public void setUserId(String userId) {
    		this.userId = userId;
    	}
    	public String getModuleName() {
    		return moduleName;
    	}
    	public void setModuleName(String moduleName) {
    		this.moduleName = moduleName;
    	}
    	public String getOperate() {
    		return operate;
    	}
    	public void setOperate(String operate) {
    		this.operate = operate;
    	}
    	
    	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
    	public Date getTime() {
    		return time;
    	}
    	public void setTime(Date time) {
    		this.time = time;
    	}
    	
    	public String getClassName() {
    		return className;
    	}
    	public void setClassName(String className) {
    		this.className = className;
    	}
    	public String getMethodName() {
    		return methodName;
    	}
    	public void setMethodName(String methodName) {
    		this.methodName = methodName;
    	}
    	public String getParams() {
    		return params;
    	}
    	public void setParams(String params) {
    		this.params = params;
    	}
    	public String getIp() {
    		return ip;
    	}
    	public void setIp(String ip) {
    		this.ip = ip;
    	}
    	//扩展字段
    	/**用户账号*/
    	private String username;
    	/**组织名称*/
    	private String organizationName;
    
    	public String getUsername() {
    		return username;
    	}
    	public void setUsername(String username) {
    		this.username = username;
    	}
    	public String getOrganizationName() {
    		return organizationName;
    	}
    	public void setOrganizationName(String organizationName) {
    		this.organizationName = organizationName;
    	}
    	
    

    SysUserLogMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
    <mapper namespace="com.carfi.vrcp.dao.SysUserLogMapper" >
      <resultMap id="BaseResultMap" type="com.carfi.vrcp.pojo.SysUserLog" >
        <id column="log_id" property="logId" jdbcType="VARCHAR" />
        <result column="user_id" property="userId" jdbcType="VARCHAR" />
        <result column="module_name" property="moduleName" jdbcType="VARCHAR" />
        <result column="operate" property="operate" jdbcType="VARCHAR" />
        <result column="time" property="time" jdbcType="TIMESTAMP" />
        <result column="class_name" property="className" jdbcType="VARCHAR" />
        <result column="method_name" property="methodName" jdbcType="VARCHAR" />
        <result column="params" property="params" jdbcType="VARCHAR" />
        <result column="ip" property="ip" jdbcType="VARCHAR" />
      </resultMap>
      <resultMap type="com.carfi.vrcp.pojo.SysUserLog" id="BaseResultMapExt" extends="BaseResultMap">
      	<result column="username" property="username" jdbcType="VARCHAR" />
        <result column="full_name" property="organizationName" jdbcType="VARCHAR" />
      </resultMap>
      <sql id="BaseColumn">
      	log_id, user_id, module_name, operate, time, class_name, method_name, params, ip
      </sql>
      <insert id="insertToTable">
      	insert into ${tableName}
      	(log_id, user_id, module_name, operate, time, class_name, method_name, params, ip)
      	values
      	(
      	#{log.logId,jdbcType=VARCHAR},
      	#{log.userId,jdbcType=VARCHAR},
      	#{log.moduleName,jdbcType=VARCHAR},
      	#{log.operate,jdbcType=VARCHAR},
      	#{log.time,jdbcType=TIMESTAMP},
      	#{log.className,jdbcType=VARCHAR},
      	#{log.methodName,jdbcType=VARCHAR},
      	#{log.params,jdbcType=VARCHAR},
      	#{log.ip,jdbcType=VARCHAR}
      	)
      </insert>
      <insert id="insert" parameterType="com.carfi.vrcp.pojo.SysUserLog" >
      	insert into sys_user_log 
      	(log_id, user_id, module_name, operate, time, class_name, method_name, params, ip)
      	values
      	(
      	#{logId,jdbcType=VARCHAR},
      	#{userId,jdbcType=VARCHAR},
      	#{moduleName,jdbcType=VARCHAR},
      	#{operate,jdbcType=VARCHAR},
      	#{time,jdbcType=TIMESTAMP},
      	#{className,jdbcType=VARCHAR},
      	#{methodName,jdbcType=VARCHAR},
      	#{params,jdbcType=VARCHAR},
      	#{ip,jdbcType=VARCHAR}
      	)
      </insert>
      <!-- <select id="selectAll" resultMap="BaseResultMap">
      	select 
      	<include refid="BaseColumn"/>
      	from sys_user_log
      </select> -->
       <select id="selectAll" resultMap="BaseResultMapExt" parameterType="com.carfi.vrcp.query.SysUserLogQuery">
      	SELECT
    	U.USERNAME,
    	O.FULL_NAME,
    	UL.LOG_ID,
    	UL.USER_ID,
    	UL.MODULE_NAME,
    	UL.OPERATE,
    	UL.TIME,
    	UL.CLASS_NAME,
    	UL.METHOD_NAME,
    	UL.PARAMS,
    	UL.IP
    	FROM
    		V_USER_LOG UL
    	INNER JOIN SYS_USER U ON U.USER_ID = UL.USER_ID
    	INNER JOIN SYS_ORGANIZATION O ON O.ORGANIZATION_ID = U.ORGANIZATION_ID
    	<where>
    		<if test="organizationId != null and organizationId != ''">
    			AND O.ORGANIZATION_ID = #{organizationId,jdbcType=VARCHAR}
    		</if>
    		<if test="organizationIds != null">
    			AND O.ORGANIZATION_ID IN 
    			<foreach collection="organizationIds" item="oid" open="(" separator="," close=")">
    				#{oid,jdbcType=VARCHAR}
    			</foreach>
    		</if>
    		<if test="username != null and username != ''">
    			AND UL.USER_ID LIKE CONCAT('%',#{username,jdbcType=VARCHAR},'%')
    		</if>
    		<if test="moduleName != null and moduleName != ''">
    			AND UL.MODULE_NAME LIKE CONCAT('%',#{moduleName,jdbcType=VARCHAR},'%')
    		</if>
    	</where>
    	ORDER BY UL.TIME DESC 
      </select>
      <select id="selectAllByTables" resultMap="BaseResultMapExt">
      	SELECT
    	U.USERNAME,
    	O.FULL_NAME,
    	UL.LOG_ID,
    	UL.USER_ID,
    	UL.MODULE_NAME,
    	UL.OPERATE,
    	UL.TIME,
    	UL.CLASS_NAME,
    	UL.METHOD_NAME,
    	UL.PARAMS,
    	UL.IP
    	FROM
    		${table} UL
    	INNER JOIN SYS_USER U ON U.USER_ID = UL.USER_ID
    	INNER JOIN SYS_ORGANIZATION O ON O.ORGANIZATION_ID = U.ORGANIZATION_ID
    	<where>
    		<if test="query.organizationId != null and query.organizationId != ''">
    			AND O.ORGANIZATION_ID = #{query.organizationId,jdbcType=VARCHAR}
    		</if>
    		<if test="query.organizationIds != null">
    			AND O.ORGANIZATION_ID IN 
    			<foreach collection="query.organizationIds" item="oid" open="(" separator="," close=")">
    				#{oid,jdbcType=VARCHAR}
    			</foreach>
    		</if>
    		<if test="query.username != null and query.username != ''">
    			AND U.USERNAME LIKE CONCAT('%',#{query.username,jdbcType=VARCHAR},'%')
    		</if>
    		<if test="query.moduleName != null and query.moduleName != ''">
    			AND UL.MODULE_NAME LIKE CONCAT('%',#{query.moduleName,jdbcType=VARCHAR},'%')
    		</if>
    		<if test="query.startTime != null and query.endTime != null">
    			AND UL.TIME BETWEEN #{query.startTime,jdbcType=TIMESTAMP} AND #{query.endTime,jdbcType=TIMESTAMP}
    		</if>
    	</where>
    	ORDER BY UL.TIME DESC 
      </select>
    </mapper>
    
    

    SysUserLogMapper.java

    public interface SysUserLogMapper {
    
    	/**
    	 * 新增日志记录
    	 * @param sysUserLog
    	 * @return
    	 */
    	int insert(SysUserLog sysUserLog);
    	/**
    	 * 添加日志到指定数据库
    	 * @param tableName
    	 * @param sysUserLog
    	 * @return
    	 */
    	int insertToTable(@Param("tableName") String tableName,@Param("log") SysUserLog sysUserLog);
    	/**
    	 * 查询所有
    	 * @return
    	 */
    	List<SysUserLog> selectAll(SysUserLogQuery query);
    	
    	
    	List<SysUserLog> selectAllByTables(@Param("table") String table,@Param("query") SysUserLogQuery query);
    }
    

    SysUserLogService.java

    PageInfo 为Mybatis分页插件“PageHelp”中的分页辅助类

    public interface SysUserLogService {
    
        /**
         * 添加系统用户日志
         *
         * @param sysUserLog 日志信息
         */
        void addUserLog(SysUserLog sysUserLog);
    
        /**
         * 分页获取日志列表
         *
         * @param query  查询参数
         * @param start  页数
         * @param length 每页个数
         * @return
         */
        PageInfo queryPage(SysUserLogQuery query, Integer start, Integer length) throws Exception;
    

    SysUserLogServiceImpl.java

    SysCacheUtil:项目中集成了EhCahe缓存,而后根据项目的缓存规则封装的缓存工具类。在该日志查询、存储方案中将根据数据库中的日志表进行操作,顾将日志数据表名存入缓存。查询系统数据库中日志数据表表名的sql语句如下:
    SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE=‘BASE TABLE’ AND TABLE_SCHEMA = ‘数据库名称’ AND TABLE_NAME LIKE CONCAT (‘sys_user_log’,’%’);

    
    /**
     * 用户日志管理业务实现
     *
     * @author jiangliuhong
     * @CREATEDATE 2017年2月13日
     */
    @Service("sysUserLogService")
    public class SysUserLogServiceImpl implements SysUserLogService {
    
        @Autowired
        private SysUserLogMapper userLogMapper;
        @Autowired
        private DbService dbService;
    
        /**
         * sys_user_log
         */
        private static String preTableName = "sys_user_log";
    
        @Override
        public void addUserLog(SysUserLog sysUserLog) {
            try {
                userLogMapper.insertToTable(getUserLogTableName(), sysUserLog);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public PageInfo queryPage(SysUserLogQuery query, Integer start, Integer length) throws Exception {
            // 判断开始结束日期是否为空
            if (query.getStartTime() == null || query.getEndTime() == null) {
                throw new CustomerException("开始、结束时间不能为空");
            }
    		// 从缓存中读取系统数据库中的日志表表名
            List<String> userLogTableName = (List<String>) SysCacheUtil.getSysUserLogTableName();
            // 检查系统用户日志表名缓存是否存在,不存在则查询
            if (userLogTableName == null) {
                userLogTableName = dbService.queryUserLogTableName();
            }
            String logTable = "";
            if (userLogTableName == null ? true : userLogTableName.size() > 0) {
                // 标记是否比较开始时间
                Boolean isStart = true;
                for (int i = 0; i < userLogTableName.size(); i++) {
                    if (isStart) {
                        // 如果表创建时间与开始时间时间同年同月 则使循环体往下执行
                        if (DateUtil.equals(changeToDate(userLogTableName.get(i)), query.getStartTime())
                                || DateUtil.countSecond(changeToDate(userLogTableName.get(i)), query.getStartTime()) >= 0) {
                            logTable = " ( select * from " + preTableName;
                            isStart = false;
                        } else {
                            continue;
                        }
                    }
                    logTable = logTable + " union all " + " select * from " + userLogTableName.get(i);
                    // 如果表创建时间与结束时间同年同月 则跳出循环
                    if (DateUtil.equals(changeToDate(userLogTableName.get(i)), query.getEndTime())) {
                        break;
                    }
                }
                if (!isStart) {
                    logTable = logTable + " ) ";
                } else {
                    logTable = preTableName;
                }
            } else {
                logTable = preTableName;
            }
            // 分页计算
            start = start / length + 1;
            PageHelper.startPage(start, length);
            // List<SysUserLog> userLogs = userLogMapper.selectAll(query);
            List<SysUserLog> userLogs = userLogMapper.selectAllByTables(logTable, query);
            PageInfo pageInfo = new PageInfo(userLogs);
            return pageInfo;
        }
    
        /**
         * 获取最新的日志表表名
         *
         * @return
         */
        private String getUserLogTableName() {
            //取用户日志表名集合
            List<String> userLogTableName = SysCacheUtil.getSysUserLogTableName();
            // 判断最新表名是否与该月同月,不同月则创建该月日志表
            if (userLogTableName == null ? true : userLogTableName.size() <= 0) {
                dbService.createTable("sys_user_log", true);
                userLogTableName = SysCacheUtil.flushSysUserLogTableName();
            } else {
                // userLogTableName.size() > 0
                String tableName = userLogTableName.get(userLogTableName.size() - 1);
                if (!DateUtil.equals(changeToDate(tableName), new Date(System.currentTimeMillis()))) {
                    // 不同月,则创建该月表,并更新日志缓存
                    dbService.createTable("sys_user_log", true);
                    userLogTableName = SysCacheUtil.flushSysUserLogTableName();
                }
            }
            if (userLogTableName == null ? true : userLogTableName.size() <= 0) {
                return preTableName;
            }
    
            // 更新缓存
            SysCacheUtil.setSysUserLogTableName(userLogTableName);
            return userLogTableName.get(userLogTableName.size() - 1);
        }
    
        /**
         * 获取名称后缀<br>
         * 格式: 20170301
         *
         * @return
         */
        @SuppressWarnings("unused")
        private String getSufName() {
            Date d = new Date();
            String s = DateUtil.date2String(d, false);
            s = s.replace("-", "");
            s = s.substring(0, s.length() - 2);
            s = s + "01";
            return s;
        }
    
        /**
         * 计算日志表时间
         *
         * @param tabelName 表名
         * @return
         */
        private Date changeToDate(String tabelName) {
            int lastIndexOf = tabelName.lastIndexOf('_');
            if (lastIndexOf >= 0) {
                tabelName = tabelName.substring(lastIndexOf + 1);
                String strDate = tabelName.substring(0, 4) + "-" + tabelName.substring(4, 6) + "-"
                        + tabelName.substring(6, 8);
                return DateUtil.string2Date(strDate);
            } else {
                return null;
            }
        }
    }
    
    

    SysUserLogQuery.java

    该类为日志表辅助查询类,具体查询条件根据项目实际情况而定

    import java.util.Date;
    import java.util.List;
    
    /**
     * 日志查询类
     */
    public class SysUserLogQuery {
    
    	/**组织id*/
    	private String organizationId;
    	/**组织集合*/
    	private List<String> organizationIds;
    	/**开始时间*/
    	private Date startTime;
    	/**结束时间*/
    	private Date endTime;
    	/**用户名*/
    	private String username;
    	/**模块名称*/
    	private String moduleName;
    	public String getOrganizationId() {
    		return organizationId;
    	}
    	public void setOrganizationId(String organizationId) {
    		this.organizationId = organizationId;
    	}
    	public List<String> getOrganizationIds() {
    		return organizationIds;
    	}
    	public void setOrganizationIds(List<String> organizationIds) {
    		this.organizationIds = organizationIds;
    	}
    	public Date getStartTime() {
    		return startTime;
    	}
    	public void setStartTime(Date startTime) {
    		this.startTime = startTime;
    	}
    	public Date getEndTime() {
    		return endTime;
    	}
    	public void setEndTime(Date endTime) {
    		this.endTime = endTime;
    	}
    	public String getUsername() {
    		return username;
    	}
    	public void setUsername(String username) {
    		this.username = username;
    	}
    	public String getModuleName() {
    		return moduleName;
    	}
    	public void setModuleName(String moduleName) {
    		this.moduleName = moduleName;
    	}
    	
    }
    
    

    DateUtil.java

    该类为自行封装的时间处理工具类,代码如下:

    import java.sql.Timestamp;
    import java.text.DateFormat;
    import java.text.ParseException;
    import java.text.SimpleDateFormat;
    import java.util.Calendar;
    import java.util.Date;
    
    public class DateUtil {
    
        public static Date string2Date(String time) {
            try {
                Date date = new Date();
                DateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                if (time.length() <= 10) {
                    time = time + " 00:00:00";
                }
                date = sdf.parse(time);
                return date;
            } catch (ParseException e) {
                e.printStackTrace();
                return null;
            }
        }
    
        /**
         * @param date 时间对象
         * @param hms  是否显示时分秒
         * @return
         */
        public static String date2String(Date date, Boolean hms) {
            DateFormat sdf;
            if (hms) {
                sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            } else {
                sdf = new SimpleDateFormat("yyyy-MM-dd");
            }
            String time = sdf.format(date);
            return time;
        }
    
        public static Timestamp string2Timestamp(String time) {
            Timestamp ts = new Timestamp(System.currentTimeMillis());
            if (time.length() <= 10) {
                time = time + " 00:00:00";
            }
            ts = Timestamp.valueOf(time);
            return ts;
        }
    
        /**
         * @param ts  时间戳对象
         * @param hms 是否显示时分秒
         * @return
         */
        public static String timestamp2String(Timestamp ts, Boolean hms) {
            DateFormat sdf;
            if (hms) {
                sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            } else {
                sdf = new SimpleDateFormat("yyyy-MM-dd");
            }
            String tsStr = "";
            tsStr = sdf.format(ts);
            return tsStr;
        }
    
        public static Date timestamp2Date(Timestamp ts) {
            Date date = new Date();
            date = ts;
            return date;
        }
    
        public static Timestamp date2Timestamp(Date date) {
            String time = "";
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            time = sdf.format(date);
            Timestamp ts = Timestamp.valueOf(time);
            return ts;
        }
    
        /**
         * 计算两个时间之间的小时差<br>
         * start - stop
         *
         * @param start 开始时间
         * @param stop  结束时间
         * @return
         */
        public static Long countHour(Date start, Date stop) {
            long diff = start.getTime() - stop.getTime();
            long hour = diff / (60 * 60 * 1000);
            return hour;
        }
    
        /**
         * 计算两个时间之间的分钟数差 <br>
         * start - stop
         *
         * @param start 开始时间
         * @param stop  结束时间
         * @return
         */
        public static Long countMinute(Date start, Date stop) {
            long diff = start.getTime() - stop.getTime();
            long min = diff / (60 * 1000);
            return min;
        }
    
        /**
         * 计算两个时间之间的秒数差<br>
         * start - stop
         *
         * @param start 开始时间
         * @param stop  结束时间
         * @return
         */
        public static Long countSecond(Date start, Date stop) {
            long diff = start.getTime() - stop.getTime();
            long sec = diff / 1000;
            return sec;
        }
    
        /**
         * 按天增加或减时间
         *
         * @param date
         * @param days  增减的天数
         * @param hms   是否显示时分秒
         * @param isAdd 加减标识,false 是减,true是加
         * @return
         */
        public static String addOrMinusDate(Date date, int days, Boolean hms, Boolean isAdd) {
            long d = (long) days;
            SimpleDateFormat df = null;
            if (hms) {
                df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            } else {
                df = new SimpleDateFormat("yyyy-MM-dd");
            }
            if (!isAdd) {
                return df.format(new Date(date.getTime() - (d * 24 * 60 * 60 * 1000)));
            } else {
                return df.format(new Date(date.getTime() + (d * 24 * 60 * 60 * 1000)));
            }
        }
    
        /**
         * 判断两个日期是否同年同月
         *
         * @param date1 时间1
         * @param date2 时间2
         * @return
         */
        public static boolean equals(Date date1, Date date2) {
            Calendar calendar1 = Calendar.getInstance();
            calendar1.setTime(date1);
            Calendar calendar2 = Calendar.getInstance();
            calendar2.setTime(date2);
            return calendar1.get(Calendar.YEAR) == calendar2.get(Calendar.YEAR)
                    && calendar1.get(Calendar.MONTH) == calendar2.get(Calendar.MONTH);
        }
    
    

    2.2 用户日志的记录

    用户日志的记录,主要通过自定义java注解,通过在service方法标记注解,使用spring aop进行日志存储

    2.2.1 自定义java注解

    自定义注解主要包括模块名称、操作内容两个内容,其使用方式为:@LogAnnotation(moduleName = “角色管理”, operate = “新增角色”)
    如果需要其他内容,可根据以下源码进行扩展

    LogAnnotation.java

    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 自定义日志注解
     */
    @Retention(RetentionPolicy.RUNTIME)  
    @Target({ElementType.METHOD})  
    @Documented
    public @interface LogAnnotation {
    	
    	/**模块名称*/
        String moduleName() default "";  
        /**操作内容*/
        String operate() default "";  
    	
    }
    

    注解使用:

    @LogAnnotation(moduleName = "角色管理", operate = "新增角色")
    @Override
    public void saveRole(SysRole role, String[] perIds) {
    

    2.2.2 切面配置

    aop配置

    	<aop:config>
    	    <aop:pointcut id="pc" expression="execution(* com.carfi.vrcp.service.*.*.*(..))"/> 
    	    <!--把事务控制在Service层-->
       <aop:advisor pointcut-ref="pc" advice-ref="txAdvice" />
      </aop:config>
      <bean id="logInterceptor" class="XXXX.XXXX.XXX.LogInterceptor"></bean>
      <aop:config>  
        <aop:aspect id="logAop" ref="logInterceptor" >
        	<aop:pointcut expression="execution(* com.carfi.vrcp.service.*.*.*(..))" id="logPoint"/>
        	<aop:after-returning method="afterReturning" pointcut-ref="logPoint" />
        </aop:aspect>
      </aop:config>
    

    LogInterceptor.java

    定义拦截器,重写afterReturning方法,在调用service方法完成之后触发该方法实现日志写入

    /**
     * 日志拦截器
     * 
     */
    public class LogInterceptor {
    	/**
    	 * 方法正常完成后执行方法
    	 * @param point
    	 */
    	@SuppressWarnings("rawtypes")
    	public void afterReturning(JoinPoint point) {
            try {
            	Subject subject = null ;
            	try {
    				subject = CarfiUserUtil.getSubject();
    			} catch (Exception e) {
    				e.printStackTrace();
    				//发生异常则不进行
    				return ;
    			}
            	SysUserLog userLog = new SysUserLog();
            	String targetName = point.getTarget().getClass().getName();
    			Class targetClass = Class.forName(targetName);
    			String methodName = point.getSignature().getName();
    			Method[] method = targetClass.getMethods();
    			Object[] params = point.getArgs(); // 获得参数列表
    			for (Method m : method) {
    				if (m.getName().equals(methodName)) {
    					Class[] tmpCs = m.getParameterTypes();
    					if (tmpCs.length == params.length) {
    						// 获取注解内容
    						LogAnnotation logAnnotation = m.getAnnotation(LogAnnotation.class);
    						if(logAnnotation != null){
    							//写入参数
    							if(params.length>0){
    								// 使用json转换工具 将参数转为json串,以便存入数据库
    								String jsonStr = JsonUtil.toJSONStr(params);
    								userLog.setParams(jsonStr);
    							}
    							//获取模块名称
    							String moduleName = logAnnotation.moduleName();
    							//获取操作名称
    							String operate = logAnnotation.operate();
    							userLog.setModuleName(moduleName);
    							userLog.setOperate(operate);
    							userLog.setClassName(targetName);
    							userLog.setMethodName(methodName);
    							userLog.setLogId(CommonUtil.generatePrimaryKey());
    							userLog.setTime(new Date());
    							HttpServletRequest request = (HttpServletRequest)((WebSubject)subject).getServletRequest();
    							userLog.setIp(CommonUtil.getIpAddr(request));
    							userLog.setUserId(CarfiUserUtil.getSysUser().getUserId());
    							SysUserLogService userLogService = (SysUserLogService) SpringUtil.getBean ("sysUserLogService");
    							userLogService.addUserLog(userLog);
    							break;
    						}
    					}
    				}
    			}
    		} catch (ClassNotFoundException e) {
    			e.printStackTrace();
    		}
    	}
    }
    

    3 SpringUtil.java 解释

    SpringUtil的作用为在非Spring IOC容器下获取Spring IOC容器的上下文环境中的类,比如获取某个bean,最常见的如本文中的多次出现的“SysUserLogService userLogService = (SysUserLogService) SpringUtil.getBean (“sysUserLogService”);”通过SpringUtil得到service

    3.1 SpringUtil 配置详解

    SpringUtil.java

    import org.springframework.beans.BeansException;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.ApplicationContextAware;
    
    /**
     * Spring工具类
     */
    public class SpringUtil implements ApplicationContextAware {
    
    	/** 上下文 */
    	private static ApplicationContext applicationContext;
    
    	public static ApplicationContext getApplicationContext() {
    		return applicationContext;
    	}
    
    	@Override
    	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    		SpringUtil.applicationContext = applicationContext;
    	}
    
    	/**
    	 * 根据Bean ID获取Bean
    	 * 
    	 * @param beanId
    	 * @return
    	 */
    	public static Object getBean(String beanId) {
    		if(applicationContext == null){
    			return null;
    		}
    		return applicationContext.getBean(beanId);
    	}
    }
    

    spring 配置文件(application.xml)中配置相应的bean,在配置文件中注册的SpringUtil bean 应在spring扫描配置之前

     <bean id="springUtil" class="XXX.XXXX.SpringUtil"></bean>
    
     <context:component-scan base-package="XXX.XXX.service"></context:component-scan>
    
    展开全文
  • 架构设计:负载均衡层设计方案(3)——Nginx进阶

    万次阅读 多人点赞 2015-07-10 14:41:41
    上篇文章《架构设计:负载均衡层设计方案(2)——Nginx安装》(http://blog.csdn.net/yinwenjie/article/details/46620711),我们介绍了Nginx的核心设计思想、基本安装和使用。本来准备继续介绍Nginx的几个使用...

    上篇文章《架构设计:负载均衡层设计方案(2)——Nginx安装》(http://blog.csdn.net/yinwenjie/article/details/46620711),我们介绍了Nginx的核心设计思想、基本安装和使用。本来准备继续介绍Nginx的几个使用特性,但是奈何博文篇幅太长,只有将一篇文章拆成两篇。本文我们将承接上文,继续讲解Nginx的实用特性,包括gzip功能、rewirte功能和一个第三方的节点监测模块。本文我们还将提到Taobao团队对Nginx的深度改造Tengine。

    1、Nginx继续进阶

    1.1、gzip

    nginx对返回给浏览器的http response body是可以进行压缩的。虽然需要消耗一点cpu和内存资源,但是想到100KB的数据量可以压缩到60KB甚至更小进行传输,是否有一定的吸引力?这里我的建议是,不要为了节约成本将业务服务和负载层服务放在一台物理服务器上,这样做既影响性能又增加了运维难度。http返回数据进行压缩的功能在很多场景下都实用:

    • 如果浏览器使用的是3G/4G网络,那么流量对于用户来说就是money。

    • 压缩可节约服务器机房的对外带宽,为更多用户服务。按照目前的市场价良好的机房带宽资源的一般在200RMB/Mbps,而服务器方案的压力往往也来自于机房带宽。

    • 主要注意的是,不是Nginx开启了gzip功能,HTTP响应的数据就一定会被压缩,除了满足Nginx设置的“需要压缩的http格式”以外,客户端(浏览器)也需要支持gzip(不然它怎么解压呢),一个好消息是,目前大多数浏览器和API都支持http压缩。

      我们首先来讲解Nginx中的gzip的设置参数,然后我们讲解当开启压缩功能后,HTTP的交互过程和过程中关键的几个属性。我们首先来看看Nginx中开启gzip的属性(gzip的设置放置在HTTP主配置区域):

      #开启gzip压缩服务,
      gzip on;

      #gzip压缩是要申请临时内存空间的,假设前提是压缩后大小是小于等于压缩前的。例如,如果原始文件大小为10K,那么它超过了8K,所以分配的内存是8 * 2 = 16K;再例如,原始文件大小为18K,很明显16K也是不够的,那么按照 8 * 2 * 2 = 32K的大小申请内存。如果没有设置,默认值是申请跟原始数据相同大小的内存空间去存储gzip压缩结果。
      gzip_buffers 2 8k;

      #进行压缩的原始文件的最小大小值,也就是说如果原始文件小于5K,那么就不会进行压缩了
      gzip_min_length 5K;

      #gzip压缩基于的http协议版本,默认就是HTTP 1.1
      gzip_http_version 1.1;

      # gzip压缩级别1-9,级别越高压缩率越大,压缩时间也就越长CPU越高
      gzip_comp_level 5;

      #需要进行gzip压缩的Content-Type的Header的类型。建议js、text、css、xml、json都要进行压缩;图片就没必要了,gif、jpge文件已经压缩得很好了,就算再压,效果也不好,而且还耗费cpu。
      gzip_types text/HTML text/plain application/x-javascript text/css application/xml;

      设置完成后,重启nginx,即可生效。下面我们来看看浏览器和服务器进行gzip压缩的请求和处理返回过程(实际上在我的《标准Web系统的架构分层》文章中,已经有所提及):

    这里写图片描述

    • 整个请求过程来看,开启gzip和不开启gip功能,其http的请求和返回过程是一致的,不同的是参数。这个可以看看我的另外一篇文章《标准Web系统的架构分层》http://blog.csdn.net/yinwenjie/article/details/46480485

    • 当开启HTTP的gzip功能时,客户端发出http请求时,会通过headers中的Accept-Encoding属性告诉服务器“我支持gzip解压,解压格式(算法)deflate,sdch为:”。Accept-Encoding:gzip,deflate,sdch

    • 注意,不是request说自己支持解压,Nginx返回response数据的时候就一定会压缩。这还要看本次Nginx返回数据的格式是什么,如果返回数据的原始数据格式,和设置的gzip_types相符合,这时Nginx才会进行压缩。

    • Nginx返回response headers是,如果数据被压缩了,就会在Content-Encoding属性中标示gzip,表示接下来返回的response content是经过压缩的;并且在Content-Type属性中表示数据的原始格式。

    • 最后返回经过压缩的response content给客户端,客户端再进行解压。这里注意一下,在客户端发送的headers里面,有一个deflate,sdch。这是两种压缩算法,如果读者感兴趣,可以查查相关的资料(我建议查查,了解哈弗曼压缩算法对扩展自己的架构思路很有帮助)

    1.2、rewrite

    本小结内容,假定读者了解正则表达式。如果您不清楚正则表达式,请首先Google或者百度,正则表达式不在我们讨论的范围内。

    Nginx的强大在于其对URL请求的重写(重定位)。Nginx的rewrite功能依赖于PCRE Lib,请一定在Nginx编译安装时,安装Pcre lib。请参见我的上一篇文章《架构设计:负载均衡层设计方案(2)——Nginx安装》http://blog.csdn.net/yinwenjie/article/details/46620711

    我们先从讲解rewrite的关键语法,然后出示几个示例,由示例进行讲解。先来说一下Nginx中几个关键的语法:

    正在表达式匹配:

    • ~ 区分大小写进行正则表达式匹配
    • ~* 不区分大小写进行正则表达式匹配
    • !~ 区分大小写进行正则表达式不匹配
    • !~* 不区分大小写进行正则表达式不匹配

    举例说明:

    示例1:location ~* \.(jpg|gif|png|ioc|jpeg)$
    
    location是Nginx中的关键字,代表当前的URL请求值。
    以上表达式表示对URL进行不区分大小写的匹配,一旦URL以jpg或gif或png或ioc或jpeg结尾时,匹配成功。
    
    示例2:$var1 ~ ^(\d+)$
    
    var1是Nginx中使用set关键字定义的变量,以上语句代表var1和一个整数进行匹配。
    

    Nginx中的全局变量:
    从上面的各个实例中,我们已经发现Nginx是支持变量的,Nginx还内置了一些全局变量,这里列举一些比较重要的全局变量:

    • $content_length: 获取request中header部分的“Content_Length”值。
    • $content_type: 获取request中header部分的“Content_type”值。
    • $request_method: 请求方式,常用的有两种请求方式:POST、GET
    • $remote_addr: 发送请求的客户端ip
    • $remote_port: 发送请求的客户端端口
    • $request_uri: 含有参数的完整的初始URI
    • $server_addr: request到达的server的ip。
    • $server_port: 请求到达的服务器的端口号。

    rewrite语法

    rewrite regex replacement flag
    
    #regex:表示当前匹配的正则表达式。只有$url大小写相关匹配regex正则表达式,这个$url才会被rewrite进行重定向。
    
    #replacement:重定向目标规则。这个目标规则支持动态变量绑定,这个问题下文马上用示例来讲。
    
    #flag:重定向规则。
    

    rewrite中的Flag关键字

    • redirect:通知客户端重定向到rewrtie后面的地址。
    • permanent:通知客户端永久重定向到rewrtie后面的地址。
    • last:将rewrite后的地址重新在server标签执行。
    • break:将rewrite后地址重新在当前的location标签执行。

    实际上针对客户端来说,其效果是一样的,都是由客户端重新发起http请求,请求地址重新定位到replacement规则的URL地址;这里关键要讲解最常用的last和break两个关键字:

    所有的rewrite语句都是要在server中的location中书写的,如下:
    server {
        。。。。。。
        。。。。。。
        location ... {
            if(...) {
                rewirte regex replacement flag;
            }
            rewirte regex replacement flag;
        }
    }
    
    那么,break关键字说明重写的replacement地址在当前location的区域马上执行。
    last关键字说明重写的replacement地址在当前server所有的location中重新再做匹配。
    

    下面我们结合rewrite关键字和rewrite flag关键字给出典型的示例进行讲解:

    示例1:
    location ~* ^/(.+)/(.+)\.(jpg|gif|png|jpeg)$ {
        rewrite ^/orderinfo/(.+)\.(jpg|gif|png|jpeg)$   /img/$1.$2   break;
        root   /cephclient;
    }
    
    location在不进行大小写区分的情况下利用正则表达式对$url进行匹配。当匹配成功后进行rewrite重定位。
    rewrite进行重写url的规则是:regex表达式第一个括号中的内容对应$1,regex表达式第二个括号中的内容对应$2,以此类推。
    这样重定位的意义就很明确了:将任何目录下的文件名重定位到img目录下的对应文件名,
    并且马上在这个location中(注意是Nginx,而不是客户端)执行这个重写后的URL定位。
    
    
    示例2:
    server {
        。。。。
        。。。。
        location ~* ^/orderinfo/(.+)\.(jpg|gif|png|jpeg)$ {
            rewrite ^/orderinfo/(.+)\.(.+)$  /img/$1.$2   last;
        }
    
        location / {
            root   /cephclient;
        }
    }
    
    在server中,有两个location位置,当url需要访问orderinfo目录下的某一个图片时,rewrite将重写这个url,
    并且重新带入这个url到server执行,这样“location /”这个location就会执行了,并找到图片存储的目录。
    

    1.3、健康检查模块

    在本小节我们介绍一个用于Nginx对后端UpStream集群节点健康状态检查的第三方模块:nginx_upstream_check_module(https://github.com/yaoweibin/nginx_upstream_check_module)。这个模块有资料介绍是TaoBao团队开发的,但是我在GitHua上试图求证时并没有找到直接证据。

    这里需要说明的是,目前有很多Nginx模块实现Nginx对后端集群节点的健康监测,不止nginx_upstream_check_module。Nginx官方有一个模块healthcheck_nginx_upstreams也可以实现对后端节点的健康监测(https://github.com/cep21/healthcheck_nginx_upstreams有详细的安装和使用介绍)

    我们回到对nginx_upstream_check_module的讲解,要使用这个第三方模块首先您需要进行下载,然后通过patch命令将补丁打入您原有的Nginx源码中,并且重新进行编译安装。下面我们来重点讲解一下这个模块的安装和使用。

    下载nginx_upstream_check_module模块:

    wget https://codeload.github.com/yaoweibin/nginx_upstream_check_module/zip/master
    
    您也可以直接到GitHua上进行下载,还一个在linux系统上使用git命令进行下载。
    

    解压安装,并补丁打入Nginx源码

    # unzip ./nginx_upstream_check_module-master.zip
    
    注意是将补丁打入Nginx源码,不是Nginx的安装路径:
    
    # cd ./nginx-1.6.2
    
    # patch -p1 < ../nginx_upstream_check_module-master/check_1.5.12+.patch
    
    如果补丁安装成功,您将看到以下的提示信息:
    patching file src/http/modules/ngx_http_upstream_ip_hash_module.c
    patching file src/http/modules/ngx_http_upstream_least_conn_module.c
    patching file src/http/ngx_http_upstream_round_robin.c
    patching file src/http/ngx_http_upstream_round_robin.h
    
    这里请注意:在nginx_upstream_check_module官网的安装说明中,有一个打补丁的注意事项:
    If you use nginx-1.2.1 or nginx-1.3.0, the nginx upstream round robin
    module changed greatly. You should use the patch named
    'check_1.2.1.patch'.
    If you use nginx-1.2.2+ or nginx-1.3.1+, It added the upstream
    least_conn module. You should use the patch named 'check_1.2.2+.patch'.
    If you use nginx-1.2.6+ or nginx-1.3.9+, It adjusted the round robin
    module. You should use the patch named 'check_1.2.6+.patch'.
    If you use nginx-1.5.12+, You should use the patch named
    'check_1.5.12+.patch'.
    If you use nginx-1.7.2+, You should use the patch named
    'check_1.7.2+.patch'.
    
    这里我们的Nginx的版本是1.6.2,那么就应该打入check_1.5.12+.patch这个补丁
    

    重新编译安装Nginx:

    注意重新编译Nginx,要使用add-module参数将这个第三方模块安装进去:
    
    # ./configure --prefix=/usr/nginx-1.6.2/ --add-module=../nginx_upstream_check_module-master/
    
    # make && make install
    

    通过以上的步骤,第三方的nginx_upstream_check_module模块就在Nginx中准备好了。接下来我们讲解一下如何使用这个模块。首先看一下upstream的配置信息:

    upstream cluster {
        # simple round-robin
        server 192.168.0.1:80;
        server 192.168.0.2:80;
    
        check interval=5000 rise=1 fall=3 timeout=4000;
    
        #check interval=3000 rise=2 fall=5 timeout=1000 type=ssl_hello;
        #check interval=3000 rise=2 fall=5 timeout=1000 type=http;
        #check_http_send "HEAD / HTTP/1.0\r\n\r\n";
        #check_http_expect_alive http_2xx http_3xx;
    }
    

    上面的代码中,check部分就是调用nginx_upstream_check_module模块的语法:

    check interval=milliseconds [fall=count] [rise=count]
    [timeout=milliseconds] [default_down=true|false]
    [type=tcp|http|ssl_hello|mysql|ajp|fastcgi]
    

    interval:必要参数,检查请求的间隔时间。

    fall:当检查失败次数超过了fall,这个服务节点就变成down状态。

    rise:当检查成功的次数超过了rise,这个服务节点又会变成up状态。

    timeout:请求超时时间,超过等待时间后,这次检查就算失败。

    default_down:后端服务器的初始状态。默认情况下,检查功能在Nginx启动的时候将会把所有后端节点的状态置为down,检查成功后,在置为up。

    type:这是检查通信的协议类型,默认为http。以上类型是检查功能所支持的所有协议类型。

    check_http_send http_packet
    
    http_packet的默认格式为:"GET / HTTP/1.0\r\n\r\n"
    

    check_http_send设置,这个设置描述了检查模块在每次检查时,向后端节点发送什么样的信息

    check_http_expect_alive [ http_2xx | http_3xx | http_4xx | http_5xx ]
    

    这些状态代码表示服务器的HTTP响应上是OK的,后端节点是可用的。默认情况的设置是:http_2xx | http_3xx

    当您根据您的配置要求完成检查模块的配置后,请首先使用nginx -t 命令监测配置文件是否可用,然后在用nginx -s reload重启nginx。

    1.4、不得不提的tengine

    Tengine是由淘宝网发起的Web服务器项目。它在Nginx的基础上,针对大访问量网站的需求,添加了很多高级功能和特性。Tengine的性能和稳定性已经在大型的网站如淘宝网,天猫商城等得到了很好的检验。它的最终目标是打造一个高效、稳定、安全、易用的Web平台(http://tengine.taobao.org/)。

    您应该懂了,我建议您根据业务的实际情况,适时在生产环境引入Tengine。但在本博客发布时,Tengine的2.X版本还不稳定,所以建议实用1.5.2的稳定版本。请记住Tengine就是经过升读改造后的Nginx

    2、后文介绍

    花了两篇文章的功夫,终于将我想给大家讲解的nginx的实用特性讲完了,但是nginx远远不止这些特性。后面有时间我们会再回到Nginx,重点讲解针对Nginx的脚本开发,我们还会讲解Nginx和Lua的集成。但是为了不扰乱这个系列博文的时间安排,下篇文章我们将开始介绍LVS技术,争取用一篇文章的篇幅讲清楚LVS核心设计思想、单节点安装和使用。再下篇文章我们介绍Keepalived技术,以及keepalived和LVS、Nginx分别进行集成,敬请关注。

    展开全文
  • 淘宝客网站架构设计方案

    千次阅读 2013-12-02 13:29:21
    以下详细讲解了四个案例,循序渐进,最终提供一个给各位淘客参考的网站架构,来解决这个流量超限的问题。仅针对淘宝客网站初学者参考,适合对淘宝客网站开发有一定了解的人。 案例一:无缓存实时架构 ...

    做一个淘宝客网站所需要的API,TOP几乎没有任何权限限制,唯一困扰各位淘客的应该就是流量了。以下详细讲解了四个案例,循序渐进,最终提供一个给各位淘客参考的网站架构,来解决这个流量超限的问题。仅针对淘宝客网站初学者参考,适合对淘宝客网站开发有一定了解的人。

    • 案例一:无缓存实时架构

    这是一个最简单的模型。用户在访问网站的时候,程序接受用户访问请求后直接通过API获取数据,再显示在网页上。

    优点:数据的实时获取

    缺点:

    1.网站页面加载的速度慢

    2.网站访问量大的时候造成API次数超过限制,导致网站挂掉

    3.淘宝API服务器发生故障或维护,导致网站挂掉

    • 案例二:文件缓存

    这个案例中以文件的形式作为缓存,通过API取到数据序列化后并将序列化之后的数据存入文件中,一般以json的方式存储,也有的php程序中采用数组来存储。

    优点:淘宝API服务器发生故障或维护时,保证了网站的正常使用

    缺点:

    1.页面显示速度慢,主要是在用户访问页面的时候触发API请求的

    2.网站访问量大的时候造成API次数超过限制,导致网站挂掉

    • 案例三:使用memcached作为缓存

    这个方案中加入了缓存判断,程序首先从memcached取缓存中的数据,如果数据失效或者过期的话,即没有命中,那么程序就通过API去请求数据,取到数据后更新缓存,同时返回数据。如果数据存在且在有效期内的话,那么将直接返回缓存中的数据,这比用文件缓存速度快得多。硬盘快不过内存就是这个道理。

    优点:页面显示速度快,在API服务器正常的时候还能自动更新缓存数据

    缺点:

    1. 
    缓存命中率的提高有难度,有过mm集群开发经验的同学应该感受到了

    2. 
    网站访问量大的时候造成API次数超过限制网站挂掉

    3. 
    服务器重启后缓存数据全部丢失(单机的情况下)

    • 案例四:缓存+持久层结合架构

    在案例三的基础上引入一个持久存储层MySQL,这样子可以避免重启,淘宝API服务器异常的情况。

    优点:有持久层存储,数据不丢失。 服务器重启,淘宝API服务器维护等各种伤不起都是浮云。

    缺点:

    1. 
    用不多久,你会看着数据库中那高达几十GB的表而疯掉

    2. 
    网站访问量大的时候造成API次数超过限制,导致网站挂掉

    3. 数据迅速递增,查询相当缓慢

    综合上面几个典型的案例,我们不难看出淘宝客网站在架构方面或多或少存在的问题:

    1.数据实时性问题

    2.读取数据的速度问题(网站页面显示速度)

    3.缓存失效问题

    4.API次数超过限制问题

    • 在以上4个问题中,尤其以API次数超过限制问题为主,如何解决呢?看下图架构

    • 该架构中,引入开源nosql产品 
      redis。在数据每秒都发生变化的时候,关系性数据库mysql 等扛不住递增的海量新数据,而redis等可以,为什么? Key! 
      大多的时候我们不用缓存淘宝的所有数据,一个好的key设计比什么都强。

      在大多数应用中可以使用提交的参数md5的值为key。

      获取key的方法详解:

      如调用taobao.user.get接口,

      所有入参有method=taobao.user.get,session=xxx,timestamp=xxxx,format=json,app_key=123456,v=2.0,sign=ERJLJGDSFSDFSD,sign_method=md5,fields=nick,sex,nick=淘宝帐号。

      由于timestamp,sign这两个参数几乎每时每刻都在变化,而session每次授权都在变化,所以先排除session,timestamp,sign这三个参数。再按照首字母升级排列:

      app_key=12345

      fields=nick,sex

      format=json

      method=taobao.user.get

      nick=淘宝账号

      sign_method=md5

      v=2.0

      拼接字符串为:app_key12345fieldsnick,sexformatjsonmethodtaobao.user.getnick淘宝账号sign_methodmd5v2.0

      key=md5(app_key12345fieldsnick,sexformatjsonmethodtaobao.user.getnick淘宝账号sign_methodmd5v2.0)

      可以使用这样的key为32位。新的数据只能刷新缓存值,增加的缓存能有多少。

      taobao.taobaoke.items.get你能取多少页?按照不同的排序有多少?

      具体的实现思路见上面流程图。其中网关实际上就是自己搭建一个读数据的接口,所有数据都从这里单点读取,由它来

      分发。网关的数据来源有:memcached缓存, Redis数据库。 
      在memcached缓存数据失效或者没有命中的时候通过API 取数

      据,这里的API不是直接去淘宝取数据,引入了自己设定的每分钟频率,比如100次/分钟,超过的时候直接去redis 取

      数据返回,当然这里的redis最好不要设置比较长的refresh 
      time,一般5~10分钟就可以。

      淘宝客网站的架构中缓存是非常重要的一个环节,控制不好就会带来各种困扰。关键点是在部署缓存和缓存key的设计

      上。

      优点:自己搭建数据来源网关, 
      内存缓存与永久存储层的合理搭配。

      需要了解的知识:

      1. 
      Memcache合理部署

      2. 
      redis 的基本功能及安装、维护。

      建议:这里的memcached 
      部署一个缓存集群,在单点失效的情况下,数据不丢失,新机器恢复的时候自动

      从backup机器里恢复缓存。 
      Redis部署两台,一主一从。

    展开全文
  • 在上一篇《标准Web系统的架构分层》文章中,我们概述了WEB系统架构中的分层架设体系,介绍了包括负载均衡层、业务层、业务通信层、数据存储层的...从本片文章开始,我们将首先详细讲解负载均衡层的架构原理和选型场景。
  • 很明显通过前面的八篇文章的介绍,并不能覆盖负载均衡层的所有技术,但是可以作为一个引子,告诉各位读者一个学习和使用负载均衡技术的思路。...接上篇:架构设计:负载均衡层设计方案(8)——负载均衡层总结上篇
  • 述标讲解技术方案感想

    千次阅读 2015-03-26 23:59:47
    今天参加了一个系统集成项目的招标... 2、根据招标书的要求做出技术方案后,还要想想用户出这个标书的背景与实际应用情况,根据用户的招标要求提出自己的见解,招标书中哪些地方做的不够或者不合理的,我们的技术方案
  • iOS离线缓存架构设计方案

    千次阅读 2017-10-09 11:20:01
    原文发布在个人简书,更多内容欢迎关注...笔者就结合之前见过的、以及笔者自己做缓存的方式,谈一谈离线缓存的实现方案以及其中的优缺点。“今日头条”,“新浪微博”都是缓存了第一页的数据,笔者这里也是这样。无demo
  • 上篇文章《架构设计:负载均衡层设计方案(6)——Nginx + Keepalived构建高可用的负载层》我们讲解了Nginx的故障切换,并且承诺各位读者会尽快讲解 LVS + Keepalived + Nginx的安装和配置。在中间由于工作的原因,...
  • 架构设计:负载均衡层设计方案(4)——LVS原理

    万次阅读 多人点赞 2015-12-19 08:59:22
    之前我们花了两篇文章的篇幅,详细讲解了Nginx的原理、安装和特性组件。后面有时间我们还会重新回到Nginx的讲解上。从本篇文章开始,我们将开始介绍LVS技术,包括基本概念、简单使用和进阶使用。本篇文章先讨论一下...
  • 架构设计:负载均衡层设计方案(2)——Nginx安装

    万次阅读 多人点赞 2015-07-10 09:32:41
    前一篇文章中我们描述了要搭设负载均衡层的业务场景和负载均衡层搭建和扩展思路。从这篇文章开始的后几篇文章,我们将详细介绍Nginx、LVS和Nginx+Keepalived、LVS+Keepalived和LVS+Nginx+...这篇文章我们首先讲解Nginx
  • B2C自营商城的订单设计方案

    千次阅读 2018-06-01 17:19:00
    所以先讲了《B2C自营商城的商品设计方案》,这篇讲解我们的订单模块怎么设计。一、订单是什么订单的本意是指你购买商品之后生成的单据凭证,只是在电商中,它是虚拟的。主流的下单方式整个电商体系中常见的下单方式...
  • 低成本光端机设计方案

    千次阅读 2011-11-26 22:07:17
    关键词:低成本光端机设计方案 ,数字光端机方案 ,数字视频光端机方案,数字光端机设计,数字视频光端机设计   谈到低成光端机设计我们应从下面几个方面来考虑: 1. 器件成本,这个是主要考虑的内容。 2. ...
  • 《架构设计:负载均衡层设计方案(2)——Nginx安装》 文章中我们详细介绍了一致性哈希算法。并且强调了一致性Hash算法是现代系统架构中的最关键算法之一,在分布式计算系统、分布式存储系统、数据分析等众多领域中...
  • 千万级数据清洗ETL设计方案

    千次阅读 2020-11-21 16:07:17
    千万级数据清洗项目分析总结项目简介一、需求分析1. 前期需求2. 中期需求3. 后期需求二、技术支持1. MySQL2. Redis三、框架设计1. 流线型代码2. 工厂模式四、调式工作1. 线上测试五、问题回顾1....现在看看之前自己的一
  • 想知道安卓上如何进行蓝牙开发吗?想为智能硬件的开发做基础准备吗?这一章开始,我们将带你感受...本系列会浅显的讲解每一个蓝牙开发可能会用到的技术点,图文并貌,将它们串起来后,会实现一个简单的蓝牙聊天小应用。
  • 之前也写过类似的数据库设计方案,这一篇是为大家详细讲解参考京东商城围绕商品怎么来设计数据库,需要关注的细节很多,对字段进行详解,结合功能实现分析每一个字段设计的意义 大家看完这篇文章后可以看看前面四篇...
  •  目前国内市场的游戏普遍为一些带有“快餐特色的RPG”游戏居多,并且这一类型的游戏的设计思路已经发展成为游戏开发者们“月经”一样的存在,有很多人“取经”,也有很多人“传道”,设置有些人对于这一设计模式...
  • 多媒体讲解器基本型设计

    万次阅读 2016-04-28 09:45:35
    多媒体讲解器功能按照播放器功能和灯光控制功能分类。播放功能分类 简易型具备按键操作功能 TF卡升级/在线播放 U盘升级/在线播放 具备人体接近检测功能/红外/雷达 自动播放讲解功能 自动停止讲解功能 自动播放音乐/...
  • 设计模式基础讲解
  • 之前也写过类似的数据库设计方案,这一篇是为大家详细讲解参考京东商城围绕商品怎么来设计数据库,需要关注的细节很多,对字段进行详解,结合功能实现分析每一个字段设计的意义 大家看完这篇文章后可以看看前面四篇...
  • 秒杀设计详细过程:(关于后端我就详细讲解一下) 前端操作 1、秒杀开始前,秒杀按钮灰掉为“未开始”,不可点击。 2、URL在活动开始前不可露出或者生效,否则容易被使用工具绕过浏览器提前下单。导致活动还未开始...
  • 如果你还不了解Nginx和LVS的相关知识,请参见我之前的两篇文章《架构设计:负载均衡层设计方案(2)——Nginx安装》( http://blog.csdn.net/yinwenjie/article/details/46620711 )、《架构设计:负载均衡层设计...
  • 在移动应用开发中,我们经常会遇到从网络请求图片到设备上展示的场景。 如果每次都重复发起请求...其实没有一个方案可以说是完美的方案,只有最适合自己业务需求的方案,才可以说是一个好方案。 我们下面所讲解
  • 合宙4G-IPC方案在物联网云视频监控应用构架中的位置,如下图所示:     从平台方案上,合宙在Cat.4和Cat.1(合宙4G系列模块)上均支持4G-IPC对接:   合宙4G模块目前已经成功对接了Linux/RTT/Li
  • 光电隔离电路设计方案(一) 光耦亦称光电隔离器或光电耦合器,简称光耦。它是以光为媒介来传输电信号的器件,通常把发光器(红外线发光二极管LED)与受光器(光敏半导体管)封装在同一管壳内。当输入端加电信号时...
  • 详细讲解面向对象设计的六大原则

    千次阅读 2020-07-08 21:13:57
    就例如接口隔离原则,在现实系统开发中,我们很难完全遵守一个模块一个接口的设计,否则业务多了就会出现代码设计过度的情况,让整个系统变得过于庞大,增加了系统的复杂度,甚至影响自己的项目进度,得不偿失啊。...
  • 主要讲解高可用的互联网交易系统架构,包括双十一、支付宝&微博红包技术架构,以及微信红包的技术架构,希望能给各位提供价值。   概述 话说每逢双十一节或春节等节假日,对大家来讲是最欢乐的日子,可以...
  • 【软件设计】六大设计原则讲解

    万次阅读 多人点赞 2015-08-21 23:05:09
    解决方案:  遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。 ...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 115,342
精华内容 46,136
关键字:

如何讲解自己的设计方案