redis 作为一款优秀的缓存数据库, 已成为许多的公司项目开发的必备底层数据库之一了, 在我们使用 redis 时, 除了平常对五种基础数据结构进行单一操作, 有时会需要依赖 redis 来处理一段相对复杂的逻辑, 而这段逻辑可能需要通过 redis client 发送多条 redis 命令来达到我们的目的, 然而这种处理方式, 不仅效率低, 而且无法保证事务的原子性; redis 从 2.6.0 版本开始提供了一种新的解决方案, 内置 lua 解释器, 通过 redis Eval 命令来执行 lua 脚本, 达到执行自定义逻辑的 redis 命令的目的
解析
Eval 命令的基本语法如下:
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...]
如果我们想在 lua 脚本中调用 redis 的命令该如何操作? 可以在脚本中使用 redis.call()或 redis.pcall()直接调用, 两者用法类似, 只是在遇到错误时, 返回错误的提示方式不同例如:
eval "return redis.call('set',KEYS[1],'bar')" 1 foo
实例:
- 10.109:9>eval "return {KEYS[1],ARGV[1]}" 1 key1 ff
- 1) "key1"
- 2) "ff"
由于 redis 是单线程执行命令的, 因此我们需要保证我们 lua 脚本足够精简, 才不至于会阻塞 redis 线程, 因此脚本内容尽量不用循环, 避免阻塞 redis 线程, 导致后续网络请求也被阻塞
项目应用
实现功能
redis 实现消息队列先进先出, 并限制队列最大长度, 超出长度则顶出队列最后一个元素
demo 代码
- import org.junit.Test;
- import org.junit.runner.RunWith;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.boot.test.context.SpringBootTest;
- import org.springframework.boot.test.context.SpringBootTest.webEnvironment;
- import org.springframework.core.io.ClassPathResource;
- import org.springframework.data.redis.core.StringRedisTemplate;
- import org.springframework.data.redis.core.script.DefaultRedisScript;
- import org.springframework.data.redis.core.script.RedisScript;
- import org.springframework.scripting.support.ResourceScriptSource;
- import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
- import java.util.Collections;
- import java.util.concurrent.ExecutorService;
- import java.util.concurrent.Executors;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- /**
- * Created by lilm on 17-11-10.
- */
- @RunWith(SpringJUnit4ClassRunner.class)
- @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
- public class RedisDemoTest {
- private final Logger logger = LoggerFactory.getLogger(getClass());
- @Autowired
- private StringRedisTemplate redisTemplate;
- /**
- * push redis 队列脚本
- * 1. 检查队列长度是否超出配置长度
- * 2. 若超出, 弹出队列最后一个元素, 并将当前元素插入第一位
- * 3. 没超出则将当前元素插入第一位
- */
- private static DefaultRedisScript<Long> queueScript = null;
- // 创建一个锁对象
- private Lock lock = new ReentrantLock();
- private Long l = 0L;
- // 最大缓存消息数
- private final static Long MAX_CACHED_NUM = 300L;
- private final static String QUEUE_KEY = "demo-queue";
- private void push() {
- try {
- lock.lock();
- Long num = redisTemplate.execute(
- getQueueScript(), Collections.singletonList(QUEUE_KEY),
- MAX_CACHED_NUM.toString(), String.valueOf(l)
- );
- logger.info("push data:{} to queue return:{}", l, num);
- } catch (Exception e) {
- logger.error("redis error:", e);
- } finally {
- l++;
- lock.unlock();
- }
- }
- private static RedisScript<Long> getQueueScript() {
- if (queueScript == null) {
- queueScript = new DefaultRedisScript<Long>();
- queueScript.setResultType(Long.class);
- // ClassPathResource 指定路径不需要前缀 classpath:
- queueScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/queue_script.lua")));
- }
- return queueScript;
- }
- /**
- * 线程池持有三十个线程, 每个线程持续写入 100 次, 推入数据为 0~2999
- * 由于 push 方法是线程安全的, 最终 redis 中 demo-queue 的结果应该是:
- * 1. list 中总共 300 条数据
- * 2. 第一条为 2999 第 300 条为 2700, 中间数据依次加 1
- */
- @Test
- public void testQueue() {
- ExecutorService service = Executors.newFixedThreadPool(50);
- try {
- for (int i = 0; i < 30; i ++) {
- Thread t = new Thread(() -> {
- int x = 0;
- while (true) {
- if (x == 100) {
- break;
- }
- push();
- x++;
- }
- });
- try {
- service.execute(t);
- } finally {
- logger.info("子线程 {} 已开启", i + 1);
- }
- }
- logger.info("已启动所有的子线程");
- service.shutdown();
- while (true) {
- if (service.isTerminated()) {
- logger.info("所有的子线程都结束了!");
- break;
- }
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
lua 脚本内容:
-- push redis 队列脚本
-- 1. 检查队列长度是否超出配置长度
-- 2. 若超出, 弹出队列最后一个元素, 并将当前元素插入第一位
-- 3. 没超出则将当前元素插入第一位
- local num = redis.call('LLEN', KEYS[1])
- if num >= tonumber(ARGV[1]) then
- redis.call('RPOP', KEYS[1])
- num = num - 1
- end
- redis.call('LPUSH', KEYS[1], ARGV[2])
- return num + 1
redis 处理结果:
demo 代码使用 springboot+junit+spring-data-redis 实现, 附 源码地址
使用 redis 加 lua 脚本的好处是使程序逻辑更加简单, 只需调用脚本执行即可, lua 脚本执行可以减少网络延迟以及多余的传输流量, redis 在执行 lua 脚本之后会将脚本 sha1 值缓存, 下次调用时可以只携带脚本 sha1 值执行, 进一步的减小网络开销
注意
使用 redis+lua 脚本时一定要精简我们的脚本, 太过复杂的逻辑将会降低 redis 执行效率, 阻塞线程, 甚至影响到系统性能; 同时复杂的脚本一旦出现 bug, 因为是在 lua 解释器中执行将很难去排查问题
来源: https://juejin.im/entry/5aa1f7b151882555666f4239