zoukankan      html  css  js  c++  java
  • 15-综合案例

    需求分析

    在业务系统中,需要记录当前业务系统的访问日志,该访问日志包含:操作人,操作时间,访问类,访问方法,请求参数,请求结果,请求结果类型,请求时长 等信息。记录详细的系统访问日志,主要便于对系统中的用户请求进行追踪,并且在系统 的管理后台可以查看到用户的访问记录。

    记录系统中的日志信息,可以通过 Spring 框架的 AOP 来实现。具体的请求处理流程如下:

    分析性能问题

    系统中用户访问日志的数据量,随着时间的推移,这张表的数据量会越来越大,因此我们需要根据业务需求,来对
    日志查询模块的性能进行优化。

    1. 分页查询优化

    由于在进行日志查询时,是进行分页查询,那也就意味着,在查看时至少需要查询 2 次:

    • 查询符合条件的总记录数 → count 操作
    • 查询符合条件的列表数据 → 分页查询 limit 操作

    通常来说,count 都需要扫描大量的行(意味着需要访问大量的数据)才能获得精确的结果,因此是很难对该 SQL 进行优化操作的。如果需要对 count 进行优化,可以采用另外一种思路,可以增加汇总表,或者 Redis 缓存来专门记录该表对应的记录数,这样的话,就可以很轻松的实现汇总数据的查询,而且效率很高,但是这种统计并不能保证百分之百的准确 。对于数据库的操作,“快速、精确、实现简单”,三者永远只能满足其二,必须舍掉其中一个。

    2. 条件查询优化

    针对于条件查询,需要对查询条件及排序字段建立索引。

    3. 读写分离

    通过主从复制集群,来完成读写分离,使写操作走主节点,而读操作走从节点。

    4. MySQL 服务器优化

    5. 应用优化

    性能优化:分页

    优化 count

    1. 创建一张表用来记录日志表的总数据量
      create table log_counter(
          logcount bigint not null
      )engine = innodb default CHARSET = utf8;
      
    2. 在每次插入数据之后更新该表 → 触发器
      DELIMITER $
      CREATE TRIGGER oper_log_insert_trigger
      AFTER insert ON operation_log
      FOR EACH ROW
      BEGIN
          UPDATE log_counter SET logcount = logcount + 1;
      END $
      DELIMITER ;
      
    3. 在进行分页查询时的获取总记录数操作,从该表中查询即可。
      <select id="countLogFromCounter" resultType="long">
          select logcount from log_counter limit 1
      </select>
      

    优化 limit

    在进行分页时,一般通过创建覆盖索引,能够比较好的提高性能。一个非常常见而又非常头疼的分页场景就是 limit 1000000, 10,此时 MySQL 需要搜索出前 1000010 条记录后,仅仅需要返回第 1000001 到 1000010 条记录,前 1000000 记录会被抛弃,查询代价非常大。

    当点击比较靠后的页码时,就会出现这个问题,查询效率非常慢。

    SELECT * FROM operation_log LIMIT 3000000, 10;
    

    将上述 SQL 优化为:

    SELECT * FROM operation_log a,
        (SELECT id FROM operation_log ORDER BY id LIMIT 3000000, 10) b
    WHERE a.id = b.id
    

    性能优化:索引

    当根据操作人进行查询时,查询的效率很低,耗时比较长。原因就是因为在创建数据库表结构时,并没有针对于操作人字段建立索引。

    CREATE INDEX idx_user_method_return_cost ON operation_log
            (operate_user, operate_method, return_class, cost_time);
    

    同上,为了查询效率高(满足最左前缀法则),我们也需要对操作方法、返回值类型、操作耗时等字段进行创建索引,以提高查询效率。

    CREATE INDEX idx_optlog_method_return_cost ONoperation_log (operate_method, return_class, cost_time);
    
    CREATE INDEX idx_optlog_return_cost ON operation_log (return_class, cost_time);
    
    CREATE INDEX idx_optlog_cost ON operation_log (cost_time);
    

    性能优化:排序

    在查询数据时,如果业务需求中需要我们对结果内容进行了排序处理,这个时候,我们还需要对排序的字段建立适当的索引,来提高排序的效率 。

    性能优化:读写分离

    在 MySQL 主从复制的基础上,可以使用读写分离来降低单台 MySQL 节点的压力,从而来提高访问效率,读写分离的架构如下:

    对于读写分离的实现,可以通过 Spring AOP 来进行动态的切换数据源,进行操作。

    数据源配置

    db.properties

    jdbc.write.driver=com.mysql.jdbc.Driver
    jdbc.write.url=jdbc:mysql://192.168.206.1:3306/mydb_1101
    jdbc.write.username=root
    jdbc.write.password=root
    
    jdbc.read.driver=com.mysql.jdbc.Driver
    jdbc.read.url=jdbc:mysql://192.168.206.129:3306/mydb_1101
    jdbc.read.username=root
    jdbc.read.password=root
    

    applicationContext-common.xml

    <!-- 配置 MyBatis 的 Session 工厂 -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="typeAliasesPackage" value="cn.edu.nuist.pojo"/>
    </bean>
    

    applicationContext-datasource.xml

    <beans ...>
        <!-- 配置数据源 - Read -->
        <bean id="readDataSource" destroy-method="close" lazy-init="true"
                class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${jdbc.read.driver}"></property>
            <property name="jdbcUrl" value="${jdbc.read.url}"></property>
            <property name="user" value="${jdbc.read.username}"></property>
            <property name="password" value="${jdbc.read.password}"></property>
        </bean>
    
        <!-- 配置数据源 - Write -->
        <bean id="writeDataSource" destroy-method="close" lazy-init="true"
                class="com.mchange.v2.c3p0.ComboPooledDataSource">
            <property name="driverClass" value="${jdbc.write.driver}"></property>
            <property name="jdbcUrl" value="${jdbc.write.url}"></property>
            <property name="user" value="${jdbc.write.username}"></property>
            <property name="password" value="${jdbc.write.password}"></property>
        </bean>
    
        <!-- 配置动态分配的读写数据源 -->
        <bean id="dataSource" class="cn.edu.nuist.aop.datasource.ChooseDataSource" lazyinit="true">
            <property name="targetDataSources">
                <map key-type="java.lang.String" value-type="javax.sql.DataSource">
                    <entry key="write" value-ref="writeDataSource"/>
                    <entry key="read" value-ref="readDataSource"/>
                </map>
            </property>
            <property name="defaultTargetDataSource" ref="writeDataSource"/>
            <property name="methodType">
                <map key-type="java.lang.String">
                    <entry key="read" value=",get,select,count,list,query,find"/>
                    <entry key="write" value=",add,create,update,delete,remove,insert"/>
                </map>
            </property>
        </bean>
    </beans>
    

    ChooseDataSource

    public class ChooseDataSource extends AbstractRoutingDataSource {
        public static Map<String, List<String>> METHOD_TYPE_MAP = new HashMap<>();
    
        // 实现父类中的抽象方法,获取数据源名称
        protected Object determineCurrentLookupKey() {
            return DataSourceHandler.getDataSource();
        }
    
        // 设置方法名前缀对应的数据源
        public void setMethodType(Map<String, String> map) {
            for (String key : map.keySet()) {
                List<String> v = new ArrayList<>();
                String[] types = map.get(key).split(",");
                for (String type : types) {
                    if (!StringUtils.isEmpty(type)) v.add(type);
                }
                METHOD_TYPE_MAP.put(key, v);
            }
            System.out.println("METHOD_TYPE_MAP: " + METHOD_TYPE_MAP);
        }
    }
    

    DataSourceHandler

    public class DataSourceHandler {
        // 数据源名称
        public static final ThreadLocal<String> holder = new ThreadLocal<String>();
    
        // 在项目启动的时候将配置的读、写数据源加到 holder 中
        public static void putDataSource(String datasource) {
            holder.set(datasource);
        }
    
        // 从 holder 中获取数据源字符串
        public static String getDataSource() {
            return holder.get();
        }
    }
    

    DataSourceAspect

    @Aspect
    @Component
    @Order(-9999) // 值越小优先级越高
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    public class DataSourceAspect {
        protected Logger logger = LoggerFactory.getLogger(this.getClass());
    
        // 配置前置通知, 使用在aspect()上注册的切入点
        @Before("execution(* cn.itcast.service.*.*(..))")
        @Order(-9999)
        public void before(JoinPoint point) {
            String className = point.getTarget().getClass().getName();
            String method = point.getSignature().getName();
            logger.info(className + "." + method + "(" + Arrays.asList(point.getArgs())+ ")");
            try {
                for (String key : ChooseDataSource.METHOD_TYPE_MAP.keySet()) {
                    // <entry key="read" value=",get,select,count,list,query,find"/>
                    // <entry key="write" value=",add,create,update,delete,remove,insert"/>
                    for (String type : ChooseDataSource.METHOD_TYPE_MAP.get(key)) {
                        if (method.startsWith(type)) {
                            System.out.println("key : " + key);
                            DataSourceHandler.putDataSource(key);
                            break;
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    通过 @Order(-9999) 注解来控制事务管理器与该通知类的加载顺序,需要让通知类先加载,来判定使用哪个数据源。

    原理分析

    性能优化:应用优化

    1. 缓存

    可以在业务系统中使用 Redis 来做缓存,缓存一些基础性的数据,来降低关系型数据库的压力,提高访问效率。

    2. 全文检索

    如果业务系统中的数据量比较大(达到千万级别),这个时候,如果再对数据库进行查询,特别是进行分页查询,速度将变得很慢(因为在分页时首先需要 count 求合计数),为了提高访问效率,可以考虑加入Solr 或者 ElasticSearch 全文检索服务,来提高访问效率。

    3. 非关系数据库

    也可以考虑将非核心(重要)数据存在 MongoDB 中,这样可以提高插入以及查询的效率。

  • 相关阅读:
    auth系统与类视图
    中间件和上下文处理器、djangoAdmin
    Django开篇以及环境搭建
    会话保持及Form表单--Form表单
    会话保持及Form表单--cookie、session
    django模型系统综合案例-分页(手动分页、内置分页)
    django模型系统综合案例
    请求与响应
    数据迁移混乱的解决方案与pycharm乱码问题+mysql数据库大小写敏感设置
    django模型系统(三)--多对多,一对一以及跨表查询
  • 原文地址:https://www.cnblogs.com/liujiaqi1101/p/13968545.html
Copyright © 2011-2022 走看看