6. 子查询
应尽量避免子查询,下面两个 SQL 返回同样的结果集,但查询性能差几个数量级:
秒级:
select id from Favourite where product_id in (select id from Product where create_time>?)
毫秒级:
select f.id from Favourite f, Product p where p.create_time>? and p.id=f.product_id
7. 数据库特性语法
MySQL 提供了很多标准 SQL 的扩展,例如:
select ... for update
insert into ... on duplicate key update ...
insert ignore into ...
replace into ...
考虑“增加积分”这样一个功能,如果不采用特性 sql,则流程如下:
( 1 ) select qty from Point where user_id=#{userId}
( 2 ) 根据该用户的积分记录是否存在,应用程序决定后续采用 update 还是 insert。
这样做有很严重的数据正确性问题。在多线程并发的场景下,两个线程都执 行了 select 后:
( 1 ) 若发现积分记录不存在,则两个线程都插入了新的积分记录;(积分 表应该在 user_id 字段建立唯一索引避免此类严重的数据问题)
( 2 ) 若发现积分记录存在,则后面执行的那个线程覆盖了前一个线程的更 新记录;
实际上这个功能用一个 SQL 就可以解决:
( 1 ) 在 user_id 字段上建立唯一索引;
( 2 )insert into Point(user_id, qty) values(#{userId}, #{qty}) on duplicate key update qty=qty+(#{qty})
采用 select ... for update 语法也可以解决,不过这个方法需要在事务中完成:
set auto_commit=0;
select qty from Point where userId=#{userId} for update;
-- 根据上述查询结果集是否存在执行 insert 或 update
insert into Point (user_id, qty) values (#{userId}, #{qty});
update Point set qty=qty+(#{delta}) where user_id=#{userId};
commit;
以上过程存在一定死锁几率,有兴趣的可以深入研究(关键词:间隙锁)。
一.只查询必要的数据
( 1 ) 对于数据量较大的表,不分页而一次性查询出所有数据将带来灾难,你会 不定期看到 OutOfMemoryError;
( 2 ) 在 select 子句中明确指定每一个返回列是一种好的做法;
二.避免在循环中查询数据库
( 1 ) 合并查询条件,一次查询数据库得到最终数据是更好的做法;
( 2 ) 嵌套循环中查询数据库更是要杜绝;
( 3 ) 批量的增删改比循环的单条操作快上几个数量级;
三.应用层缓存
( 1 ) 变动不频繁、总量不大且经常批量访问的数据(例如:权限树、国家列 表),可以考虑缓存在 JVM 内存,并且在数据发生变动时通过 MQ 告知各 服务器刷新缓存。
( 2 ) 总量大的数据(例如:产品、用户)考虑存储在 Redis 或 Memcached 等 集中式缓存;
四.事务
( 1 ) 事务是代价极大的操作,应尽量避免使用,除非数据完整性是第一优先级需求,例如余额操作;
( 2 ) 事务中的处理过程尽量精简,处理过程需要的数据应该在事务启动之前完成准备;
( 3 ) 事务中禁止远程调用,当外部系统不可用时会让这个事务挂起直至超时失败;
( 4 ) 一个事务仅完成最核心的业务逻辑,其它相关处理可以考虑异步化。例如: 下单时让客户成功创建订单是最重要的,而返积分、发送邮件可以后续完 成,失败也不要紧只要能够重试成功;
( 5 ) 当两个启用了事务的业务流程访问共同的多个资源时,尤其要注意避免死 锁,访问资源的顺序要严格一致。假设 P1 与 P2 都需要在事务更新 Ta, Tb 表,P1 更新了 Ta 接着更新 Tb,而 P2 更新了 Tb 接着更新 Ta,此时就出 现了死锁。死锁与一般的性能问题不一样,它是代码错误,必须予以解决。
五.统计算法
( 1 ) 是否可以通过一些状态标志实现增量统计?例如客户最近 30 天的订单金 额。参见《订单统计功能设计》中 stat_date 字段的设计。
( 2 ) 在从库上统计,将统计结果更新到主库;
( 3 ) 统计的条件个数与组合变化大,是否可以通过 lucene 实现?
- [ hxw ]