15-综合案例
1. 需求分析#
在业务系统中,需要记录当前业务系统的访问日志,该访问日志包含:操作人,操作时间,访问类,访问方法,请求参数,请求结果,请求结果类型,请求时长 等信息。记录详细的系统访问日志,主要便于对系统中的用户请求进行追踪,并且在系统 的管理后台可以查看到用户的访问记录。
记录系统中的日志信息,可以通过 Spring 框架的 AOP 来实现。具体的请求处理流程如下:
2. 分析性能问题#
系统中用户访问日志的数据量,随着时间的推移,这张表的数据量会越来越大,因此我们需要根据业务需求,来对
日志查询模块的性能进行优化。
1. 分页查询优化
由于在进行日志查询时,是进行分页查询,那也就意味着,在查看时至少需要查询 2 次:
- 查询符合条件的总记录数 → count 操作
- 查询符合条件的列表数据 → 分页查询 limit 操作
通常来说,count 都需要扫描大量的行(意味着需要访问大量的数据)才能获得精确的结果,因此是很难对该 SQL 进行优化操作的。如果需要对 count 进行优化,可以采用另外一种思路,可以增加汇总表,或者 Redis 缓存来专门记录该表对应的记录数,这样的话,就可以很轻松的实现汇总数据的查询,而且效率很高,但是这种统计并不能保证百分之百的准确 。对于数据库的操作,“快速、精确、实现简单”,三者永远只能满足其二,必须舍掉其中一个。
2. 条件查询优化
针对于条件查询,需要对查询条件及排序字段建立索引。
3. 读写分离
通过主从复制集群,来完成读写分离,使写操作走主节点,而读操作走从节点。
4. MySQL 服务器优化
5. 应用优化
3. 性能优化:分页#
3.1 优化 count#
- 创建一张表用来记录日志表的总数据量
create table log_counter( logcount bigint not null )engine = innodb default CHARSET = utf8;
- 在每次插入数据之后更新该表 → 触发器
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 ;
- 在进行分页查询时的获取总记录数操作,从该表中查询即可。
<select id="countLogFromCounter" resultType="long"> select logcount from log_counter limit 1 </select>
3.2 优化 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
4. 性能优化:索引#
当根据操作人进行查询时,查询的效率很低,耗时比较长。原因就是因为在创建数据库表结构时,并没有针对于操作人字段建立索引。
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);
5. 性能优化:排序#
在查询数据时,如果业务需求中需要我们对结果内容进行了排序处理,这个时候,我们还需要对排序的字段建立适当的索引,来提高排序的效率 。
6. 性能优化:读写分离#
在 MySQL 主从复制的基础上,可以使用读写分离来降低单台 MySQL 节点的压力,从而来提高访问效率,读写分离的架构如下:
对于读写分离的实现,可以通过 Spring AOP 来进行动态的切换数据源,进行操作。
6.1 数据源配置#
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)
注解来控制事务管理器与该通知类的加载顺序,需要让通知类先加载,来判定使用哪个数据源。
6.2 原理分析#
7. 性能优化:应用优化#
1. 缓存
可以在业务系统中使用 Redis 来做缓存,缓存一些基础性的数据,来降低关系型数据库的压力,提高访问效率。
2. 全文检索
如果业务系统中的数据量比较大(达到千万级别),这个时候,如果再对数据库进行查询,特别是进行分页查询,速度将变得很慢(因为在分页时首先需要 count 求合计数),为了提高访问效率,可以考虑加入Solr 或者 ElasticSearch 全文检索服务,来提高访问效率。
3. 非关系数据库
也可以考虑将非核心(重要)数据存在 MongoDB 中,这样可以提高插入以及查询的效率。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?