Redis · lesson 10 of 14
Beginner
4Intermediate
7- 5 MULTI/EXEC Transactions and the WATCH-Based Optimistic Lock
- 6 Pub/Sub: Channels, Patterns, and the Message That Never Arrives
- 7 RDB vs AOF: What Survives a Hard Kill and What Doesn't
- 8 Replication: A Read-Only Replica, and What WAIT Actually Guarantees
- 9 ACL: a User Restricted to One Key Pattern and One Command
- 12 maxmemory and Eviction: What Redis Throws Away When It's Full
- 13 Cache-Aside with Redis: Stale Reads, Races and Stampedes
Advanced
3Also part of Redis
3Lua Scripting, EVALSHA's NOSCRIPT, and Functions That Forget to Declare no-writes
A guessed script hash fails with a real NOSCRIPT error, and a Function callable via FCALL refuses to run through FCALL_RO until it explicitly declares itself read-only — Redis won't infer that for you.
By SkillByExample · Published
A Lua script gives you one atomic round trip instead of several — and the two ways to run one
(EVAL/EVALSHA versus FUNCTION/FCALL) fail differently when used carelessly.
Atomic read-then-write in one script
docker exec redis-lesson redis-cli SET stock 5
docker exec redis-lesson redis-cli EVAL "local s = tonumber(redis.call('GET', KEYS[1])); if s > 0 then return redis.call('DECR', KEYS[1]) else return -1 end" 1 stock
OK
4
The whole “check if any stock remains, then decrement” happens on the server, atomically — no
other client’s command can run between the GET and the DECR inside this script, which is
exactly the race the WATCH-based transaction lesson needed a retry loop to handle.
docker exec redis-lesson redis-cli SET stock 0
docker exec redis-lesson redis-cli EVAL "local s = tonumber(redis.call('GET', KEYS[1])); if s > 0 then return redis.call('DECR', KEYS[1]) else return -1 end" 1 stock
OK
-1
At zero stock, the script returns -1 and never calls DECR at all — the guard and the
action are inseparable inside one script, with no window for another client to sneak in
between the check and the decrement.
EVALSHA and the bug: a hash Redis has never seen
docker exec redis-lesson redis-cli SCRIPT LOAD "return redis.call('GET', KEYS[1])"
d3c21d0c2b9ca22f82737626a27bcaf5d288f99f
SCRIPT LOAD caches the script and returns its real SHA-1 hash. Now try EVALSHA with a
plausible-looking but wrong hash:
docker exec redis-lesson redis-cli EVALSHA e0e1f9fabfc9d4800c877a703b823ac0578ff831 1 mykey
NOSCRIPT No matching script. Please use EVAL.
That hash was never actually loaded — it looks like a real SHA-1 but doesn’t correspond to any
cached script, so Redis refuses to guess. The real hash, returned by SCRIPT LOAD a moment
ago, works correctly:
docker exec redis-lesson redis-cli EVALSHA d3c21d0c2b9ca22f82737626a27bcaf5d288f99f 1 mykey
hello from lua
Functions: the bug — forgetting no-writes
# mylib.lua
#!lua name=mylib
redis.register_function('my_get', function(keys, args)
return redis.call('GET', keys[1])
end)
docker exec -i redis-lesson redis-cli -x FUNCTION LOAD < mylib.lua
docker exec redis-lesson redis-cli FCALL my_get 1 mykey
mylib
hello from lua
Works fine through FCALL. Now try the read-only variant:
docker exec redis-lesson redis-cli FCALL_RO my_get 1 mykey
ERR Can not execute a script with write flag using *_ro command.
my_get never actually writes anything — but Redis doesn’t inspect the function body to
figure that out. Without an explicit no-writes flag on registration, a function is assumed
capable of writing, and FCALL_RO refuses it outright.
The fix: declare no-writes explicitly
# mylib2.lua
#!lua name=mylib2
redis.register_function{
function_name='my_get_ro',
callback=function(keys, args) return redis.call('GET', keys[1]) end,
flags={'no-writes'}
}
docker exec -i redis-lesson redis-cli -x FUNCTION LOAD < mylib2.lua
docker exec redis-lesson redis-cli FCALL_RO my_get_ro 1 mykey
mylib2
hello from lua
Same logic, registered with flags={'no-writes'} this time — FCALL_RO now runs it, because
the function explicitly declared what it does rather than leaving Redis to assume the worst.
Practice
1. Check whether specific script hashes are cached without running them.
docker exec redis-lesson redis-cli SCRIPT EXISTS d3c21d0c2b9ca22f82737626a27bcaf5d288f99f 0000000000000000000000000000000000000000
1
0
SCRIPT EXISTS accepts multiple hashes and reports 1/0 per hash — the real one from
earlier is cached, the all-zeros placeholder obviously isn’t. Useful for a client deciding
whether it’s safe to EVALSHA or whether it needs to fall back to EVAL first.
2. List every loaded function library and its declared flags.
docker exec redis-lesson redis-cli FUNCTION LIST
library_name
mylib2
engine
LUA
functions
name
my_get_ro
description
flags
no-writes
FUNCTION LIST shows every registered library, its functions, and each function’s declared
flags — the same no-writes flag that made FCALL_RO work is visible here directly, no need
to re-read the library source to check.
3. Remove a loaded function library entirely.
docker exec redis-lesson redis-cli FUNCTION DELETE mylib2
docker exec redis-lesson redis-cli FUNCTION LIST
OK
FUNCTION DELETE removes the whole library and everything it registered — FUNCTION LIST
afterward returns nothing, confirming my_get_ro is gone along with it.
Next: clustering — hash slots, resharding, and what a cluster-aware client actually needs.