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 中,这样可以提高插入以及查询的效率。

原文地址:https://www.cnblogs.com/liujiaqi1101/p/13968545.html