Redis Lua 脚本 Java 完整 Demo

  1. 项目依赖配置

Maven pom.xml

<dependencies>
    <!-- Redis客户端 -->
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>4.3.1</version>
    </dependency>
    
    <!-- 连接池 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <version>2.11.1</version>
    </dependency>
    
    <!-- 可选:JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.14.2</version>
    </dependency>
    
    <!-- 可选:Lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.28</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

Gradle build.gradle

dependencies {
    implementation 'redis.clients:jedis:4.3.1'
    implementation 'org.apache.commons:commons-pool2:2.11.1'
    implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2'
    compileOnly 'org.projectlombok:lombok:1.18.28'
    annotationProcessor 'org.projectlombok:lombok:1.18.28'
}
  1. Redis 配置类
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;

public class RedisConfig {
    
    private static JedisPool jedisPool;
    
    static {
        initPool();
    }
    
    private static void initPool() {
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        
        // 连接池配置
        poolConfig.setMaxTotal(20);           // 最大连接数
        poolConfig.setMaxIdle(10);           // 最大空闲连接
        poolConfig.setMinIdle(5);            // 最小空闲连接
        poolConfig.setMaxWait(Duration.ofMillis(3000)); // 最大等待时间
        poolConfig.setTestOnBorrow(true);    // 借出时测试连接
        poolConfig.setTestOnReturn(true);    // 归还时测试连接
        poolConfig.setTestWhileIdle(true);   // 空闲时测试连接
        
        // 创建连接池
        jedisPool = new JedisPool(poolConfig, "localhost", 6379);
        
        // 可选的:添加关闭钩子
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            if (jedisPool != null && !jedisPool.isClosed()) {
                jedisPool.close();
            }
        }));
    }
    
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
    
    public static void returnJedis(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }
    }
    
    public static JedisPool getJedisPool() {
        return jedisPool;
    }
}
  1. Lua 脚本管理类
import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Lua脚本管理器
 */
public class LuaScriptManager {
    
    // 存储脚本的SHA值
    private static final Map<String, String> scriptCache = new HashMap<>();
    
    /**
     * 加载并缓存Lua脚本
     */
    public static String loadScript(Jedis jedis, String scriptName, String scriptContent) {
        try {
            String sha = jedis.scriptLoad(scriptContent);
            scriptCache.put(scriptName, sha);
            System.out.println("脚本加载成功: " + scriptName + ", SHA: " + sha);
            return sha;
        } catch (JedisException e) {
            System.err.println("加载脚本失败: " + scriptName);
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * 执行Lua脚本(自动处理脚本不存在的情况)
     */
    public static Object executeScript(Jedis jedis, String scriptName, 
                                      String scriptContent,
                                      int keyCount, String... params) {
        try {
            String sha = scriptCache.get(scriptName);
            if (sha == null) {
                // 如果SHA不存在,先加载脚本
                sha = loadScript(jedis, scriptName, scriptContent);
                if (sha == null) {
                    throw new RuntimeException("脚本加载失败: " + scriptName);
                }
            }
            
            try {
                // 使用EVALSHA执行
                return jedis.evalsha(sha, keyCount, params);
            } catch (JedisException e) {
                if (e.getMessage().contains("NOSCRIPT")) {
                    // 如果脚本不存在,重新加载并执行
                    System.out.println("脚本不存在,重新加载: " + scriptName);
                    sha = loadScript(jedis, scriptName, scriptContent);
                    return jedis.evalsha(sha, keyCount, params);
                }
                throw e;
            }
        } catch (JedisException e) {
            System.err.println("执行脚本失败: " + scriptName);
            e.printStackTrace();
            return null;
        }
    }
    
    /**
     * 清除脚本缓存
     */
    public static void clearScriptCache() {
        scriptCache.clear();
    }
    
    /**
     * 获取缓存的脚本SHA
     */
    public static String getScriptSha(String scriptName) {
        return scriptCache.get(scriptName);
    }
}
  1. Lua 脚本常量定义
/**
 * Lua脚本常量
 */
public class LuaScripts {
    
    // 1. 原子递增并设置过期时间
    public static final String INCR_WITH_EXPIRE = 
            "local current = redis.call('GET', KEYS[1])\n" +
            "if current == false then\n" +
            "    current = tonumber(ARGV[1])\n" +
            "    redis.call('SET', KEYS[1], current)\n" +
            "else\n" +
            "    current = tonumber(current) + 1\n" +
            "    redis.call('SET', KEYS[1], current)\n" +
            "end\n" +
            "\n" +
            "if tonumber(ARGV[2]) > 0 then\n" +
            "    redis.call('EXPIRE', KEYS[1], ARGV[2])\n" +
            "end\n" +
            "\n" +
            "return current";
    
    // 2. 分布式锁 - 获取锁
    public static final String DISTRIBUTED_LOCK_ACQUIRE = 
            "local result = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])\n" +
            "if result then\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
    
    // 3. 分布式锁 - 释放锁
    public static final String DISTRIBUTED_LOCK_RELEASE = 
            "if redis.call('GET', KEYS[1]) == ARGV[1] then\n" +
            "    return redis.call('DEL', KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";
    
    // 4. 滑动窗口限流
    public static final String SLIDING_WINDOW_RATE_LIMIT = 
            "local key = KEYS[1]\n" +
            "local window = tonumber(ARGV[1]) * 1000  -- 转换为毫秒\n" +
            "local limit = tonumber(ARGV[2])\n" +
            "\n" +
            "local currentTime = redis.call('TIME')\n" +
            "local now = tonumber(currentTime[1]) * 1000 + math.floor(tonumber(currentTime[2]) / 1000)\n" +
            "\n" +
            "local clearBefore = now - window\n" +
            "redis.call('ZREMRANGEBYSCORE', key, 0, clearBefore)\n" +
            "\n" +
            "local requestCount = redis.call('ZCARD', key)\n" +
            "\n" +
            "if requestCount < limit then\n" +
            "    redis.call('ZADD', key, now, now)\n" +
            "    redis.call('EXPIRE', key, math.floor(window / 1000))\n" +
            "    return 1  -- 允许通过\n" +
            "else\n" +
            "    return 0  -- 限流\n" +
            "end";
    
    // 5. 购物车操作
    public static final String SHOPPING_CART_OPERATION = 
            "local cartKey = KEYS[1]\n" +
            "local operation = ARGV[1]\n" +
            "local itemId = ARGV[2]\n" +
            "local quantity = tonumber(ARGV[3])\n" +
            "\n" +
            "if operation == 'add' then\n" +
            "    local current = redis.call('HGET', cartKey, itemId)\n" +
            "    if current then\n" +
            "        quantity = quantity + tonumber(current)\n" +
            "    end\n" +
            "    redis.call('HSET', cartKey, itemId, quantity)\n" +
            "    \n" +
            "elseif operation == 'remove' then\n" +
            "    redis.call('HDEL', cartKey, itemId)\n" +
            "    \n" +
            "elseif operation == 'update' then\n" +
            "    if quantity <= 0 then\n" +
            "        redis.call('HDEL', cartKey, itemId)\n" +
            "    else\n" +
            "        redis.call('HSET', cartKey, itemId, quantity)\n" +
            "    end\n" +
            "    \n" +
            "elseif operation == 'clear' then\n" +
            "    redis.call('DEL', cartKey)\n" +
            "end\n" +
            "\n" +
            "-- 返回购物车总商品数\n" +
            "local items = redis.call('HGETALL', cartKey)\n" +
            "local totalItems = 0\n" +
            "for i = 2, #items, 2 do\n" +
            "    totalItems = totalItems + tonumber(items[i])\n" +
            "end\n" +
            "\n" +
            "return totalItems";
    
    // 6. CAS操作(Compare and Set)
    public static final String COMPARE_AND_SET = 
            "local oldValue = redis.call('GET', KEYS[1])\n" +
            "if oldValue == ARGV[1] then\n" +
            "    redis.call('SET', KEYS[1], ARGV[2])\n" +
            "    return 1\n" +
            "else\n" +
            "    return 0\n" +
            "end";
    
    // 7. 批量设置键值对
    public static final String BATCH_SET = 
            "local results = {}\n" +
            "for i = 1, #KEYS do\n" +
            "    redis.call('SET', KEYS[i], ARGV[i])\n" +
            "    redis.call('EXPIRE', KEYS[i], ARGV[#ARGV])\n" +
            "    table.insert(results, 1)\n" +
            "end\n" +
            "return results";
}
  1. 业务服务类
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Response;
import redis.clients.jedis.Transaction;
import redis.clients.jedis.params.SetParams;

import java.util.Collections;
import java.util.List;
import java.util.UUID;

/**
 * Redis Lua脚本业务服务类
 */
public class RedisLuaService {
    
    private static final String LOCK_PREFIX = "lock:";
    private static final String RATE_LIMIT_PREFIX = "rate_limit:";
    private static final String CART_PREFIX = "cart:";
    
    /**
     * 1. 原子递增并设置过期时间
     */
    public Long incrementWithExpire(String key, long initialValue, long expireSeconds) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            Object result = LuaScriptManager.executeScript(
                jedis,
                "INCR_WITH_EXPIRE",
                LuaScripts.INCR_WITH_EXPIRE,
                1,  // key个数
                key,
                String.valueOf(initialValue),
                String.valueOf(expireSeconds)
            );
            return result != null ? Long.parseLong(result.toString()) : null;
        }
    }
    
    /**
     * 2. 获取分布式锁
     */
    public boolean acquireDistributedLock(String lockKey, long expireMillis) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            String lockValue = UUID.randomUUID().toString();
            
            Object result = LuaScriptManager.executeScript(
                jedis,
                "DISTRIBUTED_LOCK_ACQUIRE",
                LuaScripts.DISTRIBUTED_LOCK_ACQUIRE,
                1,
                LOCK_PREFIX + lockKey,
                lockValue,
                String.valueOf(expireMillis)
            );
            
            if (result != null && Long.parseLong(result.toString()) == 1) {
                // 成功获取锁,保存锁的值用于后续释放
                ThreadLocalLockHolder.setLockValue(lockValue);
                return true;
            }
            return false;
        }
    }
    
    /**
     * 2.1 释放分布式锁
     */
    public boolean releaseDistributedLock(String lockKey) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            String lockValue = ThreadLocalLockHolder.getLockValue();
            if (lockValue == null) {
                return false;
            }
            
            Object result = LuaScriptManager.executeScript(
                jedis,
                "DISTRIBUTED_LOCK_RELEASE",
                LuaScripts.DISTRIBUTED_LOCK_RELEASE,
                1,
                LOCK_PREFIX + lockKey,
                lockValue
            );
            
            if (result != null && Long.parseLong(result.toString()) == 1) {
                ThreadLocalLockHolder.clear();
                return true;
            }
            return false;
        }
    }
    
    /**
     * 3. 滑动窗口限流
     */
    public boolean slidingWindowRateLimit(String resource, String key, 
                                          int windowSeconds, int maxRequests) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            String rateLimitKey = RATE_LIMIT_PREFIX + resource + ":" + key;
            
            Object result = LuaScriptManager.executeScript(
                jedis,
                "SLIDING_WINDOW_RATE_LIMIT",
                LuaScripts.SLIDING_WINDOW_RATE_LIMIT,
                1,
                rateLimitKey,
                String.valueOf(windowSeconds),
                String.valueOf(maxRequests)
            );
            
            return result != null && Long.parseLong(result.toString()) == 1;
        }
    }
    
    /**
     * 4. 购物车操作
     */
    public Long shoppingCartOperation(String userId, String operation, 
                                      String itemId, Integer quantity) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            String cartKey = CART_PREFIX + userId;
            
            Object result = LuaScriptManager.executeScript(
                jedis,
                "SHOPPING_CART_OPERATION",
                LuaScripts.SHOPPING_CART_OPERATION,
                1,
                cartKey,
                operation,
                itemId,
                String.valueOf(quantity)
            );
            
            return result != null ? Long.parseLong(result.toString()) : null;
        }
    }
    
    /**
     * 5. CAS操作
     */
    public boolean compareAndSet(String key, String expectedValue, String newValue) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            Object result = LuaScriptManager.executeScript(
                jedis,
                "COMPARE_AND_SET",
                LuaScripts.COMPARE_AND_SET,
                1,
                key,
                expectedValue,
                newValue
            );
            
            return result != null && Long.parseLong(result.toString()) == 1;
        }
    }
    
    /**
     * 6. 批量设置键值对
     */
    public List<Long> batchSetWithExpire(List<String> keys, List<String> values, long expireSeconds) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            // 准备参数
            String[] params = new String[keys.size() + 1];
            for (int i = 0; i < values.size(); i++) {
                params[i] = values.get(i);
            }
            params[params.length - 1] = String.valueOf(expireSeconds);
            
            Object result = LuaScriptManager.executeScript(
                jedis,
                "BATCH_SET",
                LuaScripts.BATCH_SET,
                keys.size(),
                keys.toArray(new String[0]),
                params
            );
            
            if (result instanceof List) {
                @SuppressWarnings("unchecked")
                List<Long> list = (List<Long>) result;
                return list;
            }
            return Collections.emptyList();
        }
    }
    
    /**
     * 7. 使用管道和Lua脚本组合
     */
    public List<Object> pipelineWithLua() {
        try (Jedis jedis = RedisConfig.getJedis()) {
            // 开启管道
            jedis.pipelined();
            
            // 执行多个操作
            Transaction transaction = jedis.multi();
            
            // 操作1: 设置键值
            transaction.set("key1", "value1");
            
            // 操作2: 执行Lua脚本
            String luaScript = "return redis.call('GET', KEYS[1])";
            transaction.eval(luaScript, 1, "key1");
            
            // 操作3: 增加计数器
            transaction.incr("counter");
            
            // 执行事务
            List<Object> results = transaction.exec();
            
            return results;
        }
    }
    
    /**
     * 锁值持有器(ThreadLocal)
     */
    private static class ThreadLocalLockHolder {
        private static final ThreadLocal<String> lockValueHolder = new ThreadLocal<>();
        
        public static void setLockValue(String value) {
            lockValueHolder.set(value);
        }
        
        public static String getLockValue() {
            return lockValueHolder.get();
        }
        
        public static void clear() {
            lockValueHolder.remove();
        }
    }
}
  1. 注解式分布式锁
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;

/**
 * 分布式锁注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisLock {
    
    /**
     * 锁的key
     */
    String key();
    
    /**
     * 锁的前缀
     */
    String prefix() default "lock:";
    
    /**
     * 等待时间(默认不等待)
     */
    long waitTime() default 0;
    
    /**
     * 等待时间单位
     */
    TimeUnit waitTimeUnit() default TimeUnit.MILLISECONDS;
    
    /**
     * 锁的过期时间
     */
    long expireTime() default 30000;
    
    /**
     * 过期时间单位
     */
    TimeUnit expireTimeUnit() default TimeUnit.MILLISECONDS;
    
    /**
     * 获取锁失败时的错误信息
     */
    String errorMessage() default "获取分布式锁失败";
}

/**
 * 分布式锁切面
 */
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.UUID;

@Aspect
@Component
public class RedisLockAspect {
    
    private final RedisLuaService redisLuaService;
    
    public RedisLockAspect(RedisLuaService redisLuaService) {
        this.redisLuaService = redisLuaService;
    }
    
    @Around("@annotation(redisLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedisLock redisLock) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        
        // 生成锁的key
        String lockKey = redisLock.prefix() + redisLock.key();
        
        // 尝试获取锁
        boolean acquired = false;
        long waitTime = redisLock.waitTimeUnit().toMillis(redisLock.waitTime());
        long startTime = System.currentTimeMillis();
        
        while (System.currentTimeMillis() - startTime < waitTime) {
            if (redisLuaService.acquireDistributedLock(
                lockKey, 
                redisLock.expireTimeUnit().toMillis(redisLock.expireTime())
            )) {
                acquired = true;
                break;
            }
            
            // 等待一段时间再重试
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("获取锁时被中断", e);
            }
        }
        
        if (!acquired && waitTime > 0) {
            // 如果是等待获取锁,超时后抛出异常
            throw new RuntimeException(redisLock.errorMessage());
        } else if (!acquired) {
            // 如果不等待,直接返回null或抛出异常
            return null;
        }
        
        try {
            // 执行业务逻辑
            return joinPoint.proceed();
        } finally {
            // 释放锁
            redisLuaService.releaseDistributedLock(lockKey);
        }
    }
}
  1. 配置类(Spring Boot)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.List;

@Configuration
public class RedisConfig {
    
    /**
     * 配置RedisTemplate
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        
        // 使用Jackson序列化
        GenericJackson2JsonRedisSerializer jacksonSerializer = 
            new GenericJackson2JsonRedisSerializer();
        
        // Key使用String序列化
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        
        // Value使用JSON序列化
        template.setValueSerializer(jacksonSerializer);
        template.setHashValueSerializer(jacksonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
    
    /**
     * 注册Lua脚本Bean
     */
    @Bean
    public RedisScript<Long> incrWithExpireScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LuaScripts.INCR_WITH_EXPIRE);
        script.setResultType(Long.class);
        return script;
    }
    
    @Bean
    public RedisScript<Long> distributedLockAcquireScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LuaScripts.DISTRIBUTED_LOCK_ACQUIRE);
        script.setResultType(Long.class);
        return script;
    }
    
    @Bean
    public RedisScript<Long> rateLimitScript() {
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(LuaScripts.SLIDING_WINDOW_RATE_LIMIT);
        script.setResultType(Long.class);
        return script;
    }
}
  1. Spring Boot 服务类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class RedisLuaSpringService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RedisScript<Long> incrWithExpireScript;
    
    @Autowired
    private RedisScript<Long> distributedLockAcquireScript;
    
    @Autowired
    private RedisScript<Long> rateLimitScript;
    
    /**
     * 使用Spring RedisTemplate执行Lua脚本
     */
    public Long incrementWithExpireSpring(String key, long initialValue, long expireSeconds) {
        return redisTemplate.execute(
            incrWithExpireScript,
            Arrays.asList(key),
            String.valueOf(initialValue),
            String.valueOf(expireSeconds)
        );
    }
    
    /**
     * Spring分布式锁
     */
    public boolean tryLock(String lockKey, long expireTime, TimeUnit timeUnit) {
        String lockValue = UUID.randomUUID().toString();
        Boolean success = redisTemplate.opsForValue().setIfAbsent(
            "lock:" + lockKey,
            lockValue,
            expireTime,
            timeUnit
        );
        
        return Boolean.TRUE.equals(success);
    }
    
    /**
     * 释放锁(使用Lua脚本确保原子性)
     */
    public boolean releaseLock(String lockKey, String lockValue) {
        String script = 
            "if redis.call('get', KEYS[1]) == ARGV[1] then\n" +
            "    return redis.call('del', KEYS[1])\n" +
            "else\n" +
            "    return 0\n" +
            "end";
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Arrays.asList("lock:" + lockKey),
            lockValue
        );
        
        return result != null && result == 1;
    }
    
    /**
     * 滑动窗口限流(Spring版本)
     */
    public boolean isAllowed(String key, int windowInSec, int maxRequests) {
        Long result = redisTemplate.execute(
            rateLimitScript,
            Arrays.asList("rate_limit:" + key),
            String.valueOf(windowInSec),
            String.valueOf(maxRequests)
        );
        
        return result != null && result == 1;
    }
}
  1. 测试类
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;

@SpringBootTest
public class RedisLuaDemoTest {
    
    @Resource
    private RedisLuaService redisLuaService;
    
    @Resource
    private RedisLuaSpringService redisLuaSpringService;
    
    @Test
    public void testIncrementWithExpire() {
        System.out.println("=== 测试原子递增 ===");
        Long result = redisLuaService.incrementWithExpire("test:counter", 0, 60);
        System.out.println("递增结果: " + result);
    }
    
    @Test
    public void testDistributedLock() {
        System.out.println("=== 测试分布式锁 ===");
        
        String lockKey = "order:123";
        
        // 获取锁
        boolean acquired = redisLuaService.acquireDistributedLock(lockKey, 30000);
        System.out.println("获取锁结果: " + acquired);
        
        if (acquired) {
            try {
                System.out.println("执行业务逻辑...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放锁
                boolean released = redisLuaService.releaseDistributedLock(lockKey);
                System.out.println("释放锁结果: " + released);
            }
        }
    }
    
    @Test
    public void testRateLimit() {
        System.out.println("=== 测试限流器 ===");
        
        String resource = "login";
        String userId = "user123";
        
        for (int i = 1; i <= 15; i++) {
            boolean allowed = redisLuaService.slidingWindowRateLimit(
                resource, userId, 60, 10
            );
            System.out.println("请求" + i + ": " + (allowed ? "允许" : "限流"));
            
            if (!allowed) {
                System.out.println("触发限流,等待重试...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    @Test
    public void testShoppingCart() {
        System.out.println("=== 测试购物车 ===");
        
        String userId = "user123";
        
        // 添加商品
        Long total1 = redisLuaService.shoppingCartOperation(userId, "add", "item1", 2);
        System.out.println("添加商品后总数: " + total1);
        
        // 再次添加同一商品
        Long total2 = redisLuaService.shoppingCartOperation(userId, "add", "item1", 3);
        System.out.println("再次添加后总数: " + total2);
        
        // 添加另一商品
        Long total3 = redisLuaService.shoppingCartOperation(userId, "add", "item2", 1);
        System.out.println("添加另一商品后总数: " + total3);
        
        // 更新商品数量
        Long total4 = redisLuaService.shoppingCartOperation(userId, "update", "item1", 1);
        System.out.println("更新商品后总数: " + total4);
        
        // 移除商品
        Long total5 = redisLuaService.shoppingCartOperation(userId, "remove", "item2", 0);
        System.out.println("移除商品后总数: " + total5);
    }
    
    @Test
    public void testBatchSet() {
        System.out.println("=== 测试批量设置 ===");
        
        List<String> keys = Arrays.asList("batch:key1", "batch:key2", "batch:key3");
        List<String> values = Arrays.asList("value1", "value2", "value3");
        
        List<Long> results = redisLuaService.batchSetWithExpire(keys, values, 300);
        System.out.println("批量设置结果: " + results);
    }
    
    @Test
    public void testSpringVersion() {
        System.out.println("=== 测试Spring版本 ===");
        
        // 测试Spring版本的递增
        Long result = redisLuaSpringService.incrementWithExpireSpring("spring:counter", 0, 60);
        System.out.println("Spring递增结果: " + result);
        
        // 测试Spring版本的限流
        boolean allowed = redisLuaSpringService.isAllowed("api:test", 60, 5);
        System.out.println("Spring限流结果: " + allowed);
    }
}

/**
 * 使用注解的分布式锁示例
 */
@Service
class OrderService {
    
    @RedisLock(key = "'order:' + #orderId", 
               waitTime = 5000, 
               expireTime = 30000,
               errorMessage = "订单处理中,请稍后重试")
    public void processOrder(String orderId) {
        // 业务逻辑
        System.out.println("处理订单: " + orderId);
        try {
            Thread.sleep(2000); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    @RedisLock(key = "'payment:' + #paymentId")
    public void processPayment(String paymentId) {
        // 支付处理逻辑
        System.out.println("处理支付: " + paymentId);
    }
}
  1. 性能监控工具类
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import java.util.HashMap;
import java.util.Map;

/**
 * Redis性能监控工具
 */
public class RedisMonitor {
    
    /**
     * 监控Lua脚本执行性能
     */
    public static class LuaScriptMonitor {
        private final Map<String, ScriptStats> statsMap = new HashMap<>();
        
        public void recordExecution(String scriptName, long executionTime, boolean success) {
            ScriptStats stats = statsMap.computeIfAbsent(scriptName, k -> new ScriptStats());
            stats.totalExecutions++;
            stats.totalExecutionTime += executionTime;
            
            if (success) {
                stats.successCount++;
            } else {
                stats.failureCount++;
            }
            
            if (executionTime > stats.maxExecutionTime) {
                stats.maxExecutionTime = executionTime;
            }
            if (stats.minExecutionTime == 0 || executionTime < stats.minExecutionTime) {
                stats.minExecutionTime = executionTime;
            }
        }
        
        public ScriptStats getStats(String scriptName) {
            return statsMap.get(scriptName);
        }
        
        public void printStats() {
            System.out.println("=== Lua脚本执行统计 ===");
            statsMap.forEach((name, stats) -> {
                System.out.printf("脚本: %s%n", name);
                System.out.printf("  总执行次数: %d%n", stats.totalExecutions);
                System.out.printf("  成功次数: %d%n", stats.successCount);
                System.out.printf("  失败次数: %d%n", stats.failureCount);
                System.out.printf("  平均执行时间: %.2fms%n", 
                    stats.getAverageExecutionTime());
                System.out.printf("  最长执行时间: %dms%n", stats.maxExecutionTime);
                System.out.printf("  最短执行时间: %dms%n", stats.minExecutionTime);
                System.out.println();
            });
        }
        
        static class ScriptStats {
            long totalExecutions;
            long successCount;
            long failureCount;
            long totalExecutionTime; // 毫秒
            long maxExecutionTime;
            long minExecutionTime;
            
            double getAverageExecutionTime() {
                return totalExecutions > 0 ? 
                    (double) totalExecutionTime / totalExecutions : 0;
            }
        }
    }
    
    /**
     * 检查Redis服务器信息
     */
    public static void checkRedisInfo() {
        try (Jedis jedis = RedisConfig.getJedis()) {
            String info = jedis.info();
            System.out.println("=== Redis服务器信息 ===");
            
            // 解析重要信息
            String[] lines = info.split("\r?\n");
            for (String line : lines) {
                if (line.startsWith("redis_version") ||
                    line.startsWith("used_memory_human") ||
                    line.startsWith("connected_clients") ||
                    line.startsWith("instantaneous_ops_per_sec") ||
                    line.startsWith("total_commands_processed")) {
                    System.out.println(line);
                }
            }
        }
    }
}
  1. 最佳实践总结

11.1 使用建议

public class RedisLuaBestPractices {
    
    /**
     * 最佳实践示例
     */
    public static class BestPractices {
        
        // 1. 总是使用参数化查询,避免在脚本中拼接字符串
        public void goodPractice1(Jedis jedis) {
            // 好:使用参数传递
            String script = "return redis.call('GET', KEYS[1])";
            jedis.eval(script, 1, "key1");
            
            // 不好:在脚本中拼接
            // String badScript = "return redis.call('GET', 'key1')";
        }
        
        // 2. 限制脚本执行时间
        public void goodPractice2() {
            // Redis配置文件中设置:
            // lua-time-limit 5000  # 脚本最大执行5秒
            
            // 或者在代码中监控执行时间
            long startTime = System.currentTimeMillis();
            // 执行脚本...
            long endTime = System.currentTimeMillis();
            
            if (endTime - startTime > 5000) {
                System.err.println("脚本执行时间过长!");
            }
        }
        
        // 3. 使用SCRIPT LOAD和EVALSHA
        public void goodPractice3(Jedis jedis, String script) {
            // 首次加载脚本
            String sha = jedis.scriptLoad(script);
            
            // 后续使用SHA执行
            try {
                jedis.evalsha(sha, 1, "key1");
            } catch (Exception e) {
                // 如果脚本不存在,重新加载
                if (e.getMessage().contains("NOSCRIPT")) {
                    sha = jedis.scriptLoad(script);
                    jedis.evalsha(sha, 1, "key1");
                }
            }
        }
        
        // 4. 避免在循环中执行Redis命令
        public void goodPractice4(Jedis jedis) {
            // 好:使用一个脚本完成所有操作
            String script = 
                "for i = 1, 100 do\n" +
                "    redis.call('SET', 'key' .. i, 'value' .. i)\n" +
                "end\n" +
                "return 'done'";
            
            // 不好:在Java循环中多次调用Redis
            // for (int i = 1; i <= 100; i++) {
            //     jedis.set("key" + i, "value" + i); // 网络开销大
            // }
        }
        
        // 5. 错误处理
        public void goodPractice5(Jedis jedis) {
            try {
                Object result = jedis.eval("return redis.call('GET', KEYS[1])", 1, "key1");
                // 处理结果
            } catch (Exception e) {
                // 记录日志
                System.err.println("执行脚本失败: " + e.getMessage());
                
                // 根据错误类型进行不同处理
                if (e.getMessage().contains("BUSY")) {
                    // 脚本正在执行,重试或降级
                } else if (e.getMessage().contains("NOSCRIPT")) {
                    // 脚本不存在,重新加载
                } else {
                    // 其他错误
                    throw e;
                }
            }
        }
    }
}
  1. 完整示例:秒杀系统
import redis.clients.jedis.Jedis;

import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 秒杀系统示例
 */
public class SeckillDemo {
    
    private static final String SECKILL_SCRIPT = 
            "local productKey = KEYS[1]\n" +
            "local stockKey = KEYS[2]\n" +
            "local orderKey = KEYS[3]\n" +
            "local userId = ARGV[1]\n" +
            "\n" +
            "-- 检查库存\n" +
            "local stock = tonumber(redis.call('GET', stockKey))\n" +
            "if not stock or stock <= 0 then\n" +
            "    return 0  -- 库存不足\n" +
            "end\n" +
            "\n" +
            "-- 检查是否已购买\n" +
            "local hasBought = redis.call('SISMEMBER', orderKey, userId)\n" +
            "if hasBought == 1 then\n" +
            "    return -1  -- 重复购买\n" +
            "end\n" +
            "\n" +
            "-- 扣减库存\n" +
            "stock = stock - 1\n" +
            "redis.call('SET', stockKey, stock)\n" +
            "\n" +
            "-- 记录购买订单\n" +
            "redis.call('SADD', orderKey, userId)\n" +
            "\n" +
            "-- 设置订单过期时间\n" +
            "redis.call('EXPIRE', orderKey, 3600)\n" +
            "\n" +
            "return 1  -- 购买成功";
    
    /**
     * 初始化秒杀商品
     */
    public static void initSeckillProduct(String productId, int stock) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            jedis.set("seckill:stock:" + productId, String.valueOf(stock));
        }
    }
    
    /**
     * 执行秒杀
     */
    public static int seckill(String productId, String userId) {
        try (Jedis jedis = RedisConfig.getJedis()) {
            String productKey = "seckill:product:" + productId;
            String stockKey = "seckill:stock:" + productId;
            String orderKey = "seckill:orders:" + productId;
            
            Object result = jedis.eval(
                SECKILL_SCRIPT,
                3,
                productKey,
                stockKey,
                orderKey,
                userId
            );
            
            return result != null ? ((Long) result).intValue() : -2;
        }
    }
    
    /**
     * 并发测试秒杀
     */
    public static void concurrentTest() throws InterruptedException {
        String productId = "p001";
        initSeckillProduct(productId, 100); // 100个库存
        
        int threadCount = 200;
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService executor = Executors.newFixedThreadPool(50);
        
        System.out.println("开始并发秒杀测试...");
        System.out.println("商品: " + productId + ", 库存: 100, 并发用户: " + threadCount);
        
        for (int i = 0; i < threadCount; i++) {
            String userId = "user" + i;
            executor.execute(() -> {
                try {
                    int result = seckill(productId, userId);
                    switch (result) {
                        case 1:
                            System.out.println(userId + ": 秒杀成功");
                            break;
                        case 0:
                            System.out.println(userId + ": 库存不足");
                            break;
                        case -1:
                            System.out.println(userId + ": 重复购买");
                            break;
                        default:
                            System.out.println(userId + ": 系统错误");
                    }
                } finally {
                    latch.countDown();
                }
            });
        }
        
        latch.await();
        executor.shutdown();
        
        // 查看最终库存
        try (Jedis jedis = RedisConfig.getJedis()) {
            String stock = jedis.get("seckill:stock:" + productId);
            System.out.println("\n秒杀结束,剩余库存: " + stock);
        }
    }
    
    public static void main(String[] args) throws InterruptedException {
        concurrentTest();
    }
}

这个完整的 Java Demo 包含了:

  1. 基础配置:连接池配置、工具类
  2. Lua脚本管理:脚本加载、缓存、执行
  3. 常用场景实现:分布式锁、限流、购物车等
  4. Spring集成:Spring Boot配置和使用
  5. 测试示例:完整的测试用例
  6. 性能监控:监控脚本执行性能
  7. 最佳实践:使用建议和注意事项
  8. 完整案例:秒杀系统实现

你可以根据实际需求调整和扩展这些代码。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐