一、当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监控,发现两个关键指标:

  1. 查询响应时间P99控制在200ms内
  2. 连接池使用率保持在70%以下

当这些指标异常时,需要考虑:

  • 是否缺少合适索引
  • SQL是否写得有问题
  • 是否需要垂直/水平分表

4.2 常见陷阱清单

  1. 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..")")
  1. 事务使用不当
-- 长时间事务会锁表
db:query("BEGIN")
-- 这里执行大量操作...
db:query("COMMIT")  -- 可能导致锁等待超时

-- 应该拆分为小事务或使用乐观锁
  1. 数据类型不匹配
-- 字符串与数字比较会导致索引失效
db:query("SELECT * FROM products WHERE id = '123'")  -- 错误
db:query("SELECT * FROM products WHERE id = 123")    -- 正确

五、总结与展望

经过这些优化,我们成功将那个电商平台的查询性能从5秒降到了200毫秒以内。关键点在于:

  1. 连接池复用降低开销
  2. 合理索引加速查询
  3. 分批处理控制内存
  4. 缓存热点数据
  5. 监控驱动持续优化

未来还可以考虑:

  • 将全文搜索迁移到Elasticsearch
  • 对超大型表进行分库分表
  • 尝试基于列的存储引擎

记住,优化是个持续的过程,需要根据实际业务场景和数据增长不断调整策略。希望这些经验能帮助你在Lua数据库操作的道路上少走弯路!