Redis用Lua脚本怎么跟数据库连起来,过程和技巧分享一点想法
- 问答
- 2026-01-11 11:18:03
- 2
关于Redis用Lua脚本怎么跟数据库连起来,首先需要明确一个核心概念:Redis本身是一个内存数据库,它通常不作为数据的最终持久化存储(除非你的业务允许数据丢失),我们说的“数据库”,一般指的是像MySQL、PostgreSQL这类的关系型数据库,或者是MongoDB这类非关系型数据库,它们是数据的“源头”和“归宿”,Redis在这里扮演的角色是缓存,目的是减轻后端数据库的压力,提升读写速度。
Lua脚本在这个架构里,并不直接去“连接”MySQL这样的数据库,你不能在Redis的Lua脚本里写一句conn = mysql.connect(...),这是做不到的,Redis的Lua环境是沙盒化的,它被严格限制,只能访问Redis自身的数据和执行Redis的命令,这个过程不是“连接”,而是“协作”和“数据同步”。
整个过程可以这样理解:
想象一下,你有一个应用程序(比如一个网站的后端服务),它夹在用户请求和你的数据库之间,当用户想要读取数据时,应用程序的逻辑是这样的:
- 先问Redis(缓存):应用程序接收到请求后,首先会生成一个Key(比如
user:1001:info),然后向Redis询问:“你有这个Key的数据吗?” - 如果Redis有(缓存命中):太好了!Redis直接把数据返回给应用程序,应用程序再返回给用户,这个过程非常快,完全不需要打扰后端的数据库。
- 如果Redis没有(缓存未命中):应用程序就得辛苦一趟,自己去连接真正的数据库(MySQL等),执行SQL查询,把数据从数据库里取出来。
- 回填Redis:取到数据后,应用程序做两件事:一是把数据返回给用户,二是把这个数据按照之前那个Key,设置到Redis里,并且通常会设置一个过期时间(比如TTL为30分钟),这样,下一个请求再来问同样的问题,在接下来的30分钟内,就可以直接从Redis里拿到了。
写数据的过程也类似:
- 应用程序接收到更新数据的请求。
- 应用程序首先会去连接主数据库(MySQL),执行
UPDATE或INSERT操作,确保数据已经安全地落盘了。 - 数据库操作成功后,应用程序再去操作Redis,这里有几种策略:
- 写穿:同时更新数据库和Redis缓存。
- 写删:更新数据库,然后直接删除Redis里对应的Key,这是更常见的做法,因为它更简单,能避免数据不一致的复杂问题,删除后,下一个读请求自然会触发上面的“缓存未命中”流程,从数据库拉取最新数据并回填到Redis。
Lua脚本的技巧和用武之地在哪里呢?
Lua脚本的强项在于处理“多个Redis命令的原子性执行”,在上述的读写流程中,应用程序需要执行多个Redis命令,而Lua脚本可以让你把这些命令打包成一个原子操作,这能解决一些精妙的技巧性问题。
举几个具体的想法和技巧:
-
解决缓存击穿下的“惊群效应”:
- 场景:一个热点Key(比如秒杀商品信息)突然过期了,此时有成千上万个请求同时涌来,发现缓存失效,它们会全部冲向数据库,导致数据库瞬间压力巨大,甚至崩溃。
- Lua技巧:我们可以用Lua脚本实现一个“互斥锁”的逻辑,脚本的逻辑可以是:
if redis.call('exists', 'my_key') == 0 then-- 检查Key是否存在if redis.call('setnx', 'lock_key', 1) == 1 then-- 尝试设置一个锁Key,只有一个请求能设置成功redis.call('expire', 'lock_key', 5)-- 给锁设置一个短的过期时间,防止死锁-- 这里代表获取锁成功的请求,它应该去数据库加载数据,然后调用 setex 命令将数据存入 'my_key'redis.call('del', 'lock_key')-- 最后删除锁else-- 其他没抢到锁的请求-- 可以短暂睡眠后重试,或者直接返回一个默认值(如“加载中”),避免无谓等待endendreturn redis.call('get', 'my_key')-- 最后所有请求都从这里拿到数据(要么是刚加载的,要么是其他请求加载好的)
- 这个复杂的判断和设置过程,通过一个Lua脚本原子性地完成,确保了在高并发下,只有一个请求会去访问数据库,这比在应用层用代码实现同样的逻辑要可靠和高效得多。
-
实现复杂的原子计数或库存扣减:
- 场景:秒杀扣库存,需要先检查库存是否大于0,然后再进行扣减,这两个操作必须是原子的,否则会出现超卖。
- Lua技巧:
local stock = tonumber(redis.call('get', 'item_stock')) if stock <= 0 then return 0 -- 库存不足 else redis.call('decr', 'item_stock') return 1 -- 扣减成功 end应用程序调用这个脚本,脚本会原子性地完成“查”和“减”,完美解决并发问题,如果不用Lua,在应用层写代码,很容易出现判断库存时还有,但扣减时已经被别人扣光的情况。
-
保证数据回填时的原子性和一致性:
- 场景:在缓存未命中,从数据库拉取数据后,回填到Redis时,我们可能需要执行
SETEX key ttl value,这个操作本身是原子的,但如果你回填的数据需要经过复杂的计算或组合(比如需要先HGETALL一些字段,再组合新字段,最后HMSET),那么这个组合操作就需要Lua脚本来保证原子性,防止在组合过程中,数据被其他请求修改。
- 场景:在缓存未命中,从数据库拉取数据后,回填到Redis时,我们可能需要执行
总结一下核心想法:
不要把Lua脚本看作是连接Redis和数据库的桥梁,它没这个功能,它的角色是在Redis内部,作为一个超级粘合剂和原子操作执行器,它的价值在于,当你的应用程序需要与Redis进行“多次交互才能完成一个完整业务逻辑”时,Lua脚本能把这个“多次交互”变成一个“原子操作”。
这样做的好处非常明显:
- 原子性:解决了竞态条件,避免了脏读脏写。
- 减少网络开销:多个命令一次发送,一次返回,降低了网络延迟的影响。
- 减轻服务器压力:将复杂的逻辑从应用服务器转移到了数据所在的Redis服务器上执行,利用了Redis的高性能。
技巧的核心在于识别出你的业务场景中,哪些对Redis的操作是“一组需要原子性保证的命令序列”,然后将它们用Lua脚本封装起来,由应用程序在合适的时机(比如缓存未命中后加载数据时,或者处理高并发写请求时)去调用这个脚本,这才是Redis Lua脚本与数据库“协作”的正确方式和精髓所在。

本文由符海莹于2026-01-11发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://haoid.cn/wenda/78656.html
