一、当Lua遇上大数据量查询的烦恼
说实话,Lua在处理数据库操作时就像个灵活的杂技演员,但一旦遇到大数据量查询,这个杂技演员就容易在钢丝上摇摇晃晃。我见过不少开发者在使用Lua连接MySQL时,面对十万级以上的数据查询,整个系统就像老牛拉破车一样慢。
举个真实案例:某电商平台的商品搜索功能,最初直接用Lua-MySQL驱动执行全表扫描,结果每次查询都要5秒以上。后来我们通过分析发现,问题出在三个地方:没有合理使用索引、重复创建数据库连接、以及一次性加载全部结果集。
-- 反面教材:典型的低效查询方式
local mysql = require "resty.mysql"
local db, err = mysql:new()
db:set_timeout(1000) -- 超时设置1秒
-- 每次查询都新建连接(性能杀手)
local ok, err, errno, sqlstate = db:connect{
host = "127.0.0.1",
port = 3306,
database = "ecommerce",
user = "root",
password = "123456",
}
-- 全表扫描且没有限制返回数量
local res, err, errno = db:query("SELECT * FROM products WHERE name LIKE '%手机%'")
db:close()
二、性能优化的三板斧
2.1 连接池:别把时间浪费在握手上了
数据库连接就像谈恋爱,每次都要从"你好"开始实在太费劲。OpenResty的lua-resty-mysql驱动自带了连接池支持,这简直是性能提升的灵丹妙药。
local mysql = require "resty.mysql"
local db_pool = mysql:new()
-- 初始化连接池(只需一次)
local function init_db()
local ok, err = db_pool:connect{
host = "127.0.0.1",
-- 其他连接参数...
pool = "ecommerce_pool", -- 指定连接池名称
pool_size = 50, -- 连接池大小
}
if not ok then
ngx.log(ngx.ERR, "failed to connect: ", err)
return nil
end
return true
end
-- 使用时从连接池获取
local function query_db(sql)
local db = mysql:new()
db:set_timeout(2000)
-- 从连接池获取连接(复用现有连接)
local ok, err = db:connect{
host = "127.0.0.1",
-- 其他连接参数...
pool = "ecommerce_pool",
}
-- 执行查询...
end
2.2 索引优化:给查询装上GPS导航
没有索引的查询就像在图书馆里找书却不看目录。我们在products表的name和category字段上创建了联合索引后,查询速度提升了20倍。
-- 优化后的查询示例
local res, err = db:query([[
SELECT id, name, price FROM products
WHERE name LIKE '手机%' -- 使用左匹配而非全模糊
AND category = '电子产品'
ORDER BY sales_volume DESC
LIMIT 100 -- 限制返回数量
]])
-- 对应的MySQL索引建议:
-- ALTER TABLE products ADD INDEX idx_name_category (name, category);
-- ALTER TABLE products ADD INDEX idx_sales (sales_volume);
2.3 分批处理:别一口吃成胖子
当处理百万级数据时,我们采用分批查询策略,配合游标或分页,避免内存爆炸。
-- 分批查询实现
local batch_size = 1000
local offset = 0
local results = {}
repeat
local sql = string.format([[
SELECT id, name FROM large_table
WHERE create_time > '2023-01-01'
LIMIT %d OFFSET %d
]], batch_size, offset)
local res, err = db:query(sql)
if not res or #res == 0 then break end
-- 处理当前批次数据
for _, row in ipairs(res) do
table.insert(results, process_row(row))
end
offset = offset + batch_size
until #res < batch_size
三、高级技巧:缓存与预处理
3.1 Redis缓存热门查询
对于变化不频繁的热门数据,引入Redis作为缓存层能显著减轻数据库压力。
local redis = require "resty.redis"
local red = redis:new()
-- 先查缓存,命中则直接返回
local cache_key = "products:手机"
local cached, err = red:get(cache_key)
if cached and cached ~= ngx.null then
return cjson.decode(cached)
end
-- 缓存未命中则查询数据库
local products, err = query_products_from_db()
if not products then
return nil, err
end
-- 写入缓存(设置10分钟过期)
red:setex(cache_key, 600, cjson.encode(products))
3.2 预处理语句防注入又提效
预处理语句不仅能防SQL注入,还能让数据库缓存执行计划。
-- 使用预处理语句
local stmt = "SELECT * FROM users WHERE id = ? AND status = ?"
local res, err = db:query(stmt, {user_id, "active"})
-- 批量插入时预处理效果更明显
local bulk_stmt = "INSERT INTO logs (content, level) VALUES (?, ?)"
local params = {}
for _, log in ipairs(logs) do
table.insert(params, {log.content, log.level})
end
db:query(bulk_stmt, params)
四、实战经验与避坑指南
4.1 监控与调优不可或缺
我们在生产环境部署了Prometheus监控,发现两个关键指标:
- 查询响应时间P99控制在200ms内
- 连接池使用率保持在70%以下
当这些指标异常时,需要考虑:
- 是否缺少合适索引
- SQL是否写得有问题
- 是否需要垂直/水平分表
4.2 常见陷阱清单
- N+1查询问题:
-- 错误做法:循环内查询
local orders = get_orders()
for _, order in ipairs(orders) do
order.items = db:query("SELECT * FROM items WHERE order_id="..order.id)
end
-- 正确做法:批量查询
local order_ids = table.concat(pluck(orders, "id"), ",")
local all_items = db:query("SELECT * FROM items WHERE order_id IN ("..order_ids..")")
- 事务使用不当:
-- 长时间事务会锁表
db:query("BEGIN")
-- 这里执行大量操作...
db:query("COMMIT") -- 可能导致锁等待超时
-- 应该拆分为小事务或使用乐观锁
- 数据类型不匹配:
-- 字符串与数字比较会导致索引失效
db:query("SELECT * FROM products WHERE id = '123'") -- 错误
db:query("SELECT * FROM products WHERE id = 123") -- 正确
五、总结与展望
经过这些优化,我们成功将那个电商平台的查询性能从5秒降到了200毫秒以内。关键点在于:
- 连接池复用降低开销
- 合理索引加速查询
- 分批处理控制内存
- 缓存热点数据
- 监控驱动持续优化
未来还可以考虑:
- 将全文搜索迁移到Elasticsearch
- 对超大型表进行分库分表
- 尝试基于列的存储引擎
记住,优化是个持续的过程,需要根据实际业务场景和数据增长不断调整策略。希望这些经验能帮助你在Lua数据库操作的道路上少走弯路!
评论