springboot使用redisTemplate操作lua脚本
文章目录一、写在前面二、使用lua1、springboot使用lua1代码2解释2、redisson使用lua3、关于redis-cluster的报错RedisException: CROSSSLOT Keys in request dont hash to the same slot1优化12优化2三、常用lua1、判断手机号和身份证号不重复2、实现分布式锁一、写在前面操作redis使用Lua脚本有诸多好处减少网络开销可以将多个请求通过脚本的形式一次发送减少网络时延和请求次数。原子性的操作Redis会将整个脚本作为一个整体执行中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件无需使用事务。代码复用客户端发送的脚步会永久存在redis中这样其他客户端可以复用这一脚本来完成相同的逻辑。速度快见 与其它语言的性能比较, 还有一个 JIT编译器可以显著地提高多数任务的性能; 对于那些仍然对性能不满意的人, 可以把关键部分使用C实现, 然后与其集成, 这样还可以享受其它方面的好处。**可以移植只要是有ANSI C 编译器的平台都可以编译你可以看到它可以在几乎所有的平台上运行:从 Windows 到Linux同样Mac平台也没问题, 再到移动平台、游戏主机甚至浏览器也可以完美使用 (翻译成JavaScript)。源码小巧20000行C代码可以编译进182K的可执行文件加载快运行快。Redis中使用LuaRedis使用Lua脚本Redis中多命令保持原子性神器 - Lua二、使用lua1、springboot使用lua1代码TestpublicvoidredisLuaTest(){Stringscript1if redis.call(setnx, KEYS[1], ARGV[1]) 1 then redis.call(set, KEYS[2], ARGV[2]) return 1 else return 0 end;Longresult1(Long)redisTemplate.execute(newDefaultRedisScriptLong(script1,Long.class),Arrays.asList(key1,key2),value1,value2);System.out.println(result1);// 1// 如果key1value1则删除key1返回删除的状态否则返回0Stringscript2if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;Longresult2(Long)redisTemplate.execute(newDefaultRedisScriptLong(script2,Long.class),Arrays.asList(key1),value1);System.out.println(result2);// 1}2解释/** 第一个参数使用默认的DefaultRedisScript即可 ListK keys是key的集合 Object... args是val的集合 */OverridepublicTTexecute(RedisScriptTscript,ListKkeys,Object...args){returnscriptExecutor.execute(script,keys,args);}key的集合在lua中可以使用KEYS[1]、KEYS[2]……获取注意KEYS必须大写不能拼错val的集合在lua中可以使用ARGV[1]、ARGV[2]……获取注意ARGV必须大写不能拼错。说白了使用redisTemplate操作lua也就是传key的集合和val的集合这一串lua脚本可以保证其原子性的。具体lua语法其实也很简单基本掌握了if else、循环、赋值语句就能应付大部分操作redis的命令。lua中的redis.call命令就是操作redis的命令第一个参数就是redis的原始命令后面的参数就是redis命令的参数使用起来也非常方便。2、redisson使用lua// 判断手机号和身份证是否重复publicfinalstaticStringREPEAT_LUAlocal idCardKey grab:ticket:repeat:idcard: .. KEYS[1]\nlocal phoneKey grab:ticket:repeat:phone: .. KEYS[2]\nlocal idCardExists redis.call(EXISTS, idCardKey)\nlocal phoneExists redis.call(EXISTS, phoneKey)\nif idCardExists 0 and phoneExists 0 then\n redis.call(SET, idCardKey, 1)\n redis.call(SET, phoneKey, 1)\n return 1\nelse\n return 0\nend;publicbooleanisRepeatGrab(StringidCard,Stringphone){ListObjectkeysArrays.asList(idCard,phone);ObjectresultredissonClient.getScript().eval(RScript.Mode.READ_WRITE,CommonConstant.REPEAT_LUA,RScript.ReturnType.INTEGER,keys);return(Long)result0;}importorg.redisson.Redisson;importorg.redisson.api.RScript;importorg.redisson.api.RedissonClient;importorg.redisson.config.Config;importjava.util.ArrayList;importjava.util.List;publicclassTicketBookingExample{publicstaticbooleantryBookTicket(RedissonClientredissonClient,Stringphone,ListStringidCards){StringluaScriptlocal phoneKey KEYS[1]\nlocal idCardCount #KEYS - 1\nlocal phoneExists redis.call(EXISTS, phoneKey)\nif phoneExists 1 then\n return 0\nend\nfor i 2, #KEYS do\n local idCardKey KEYS[i]\n local idCardExists redis.call(EXISTS, idCardKey)\n if idCardExists 1 then\n return 0\n end\nend\nredis.call(SET, phoneKey, 1)\nfor i 2, #KEYS do\n local idCardKey KEYS[i]\n redis.call(SET, idCardKey, 1)\nend\nreturn 1;ListStringkeysnewArrayList();keys.add(phone);keys.addAll(idCards);RScriptscriptredissonClient.getScript();Objectresultscript.eval(RScript.Mode.READ_WRITE,luaScript,RScript.ReturnType.INTEGER,keys);return(Integer)result1;}publicstaticvoidmain(String[]args){ConfigconfignewConfig();config.useSingleServer().setAddress(redis://127.0.0.1:6379);RedissonClientredissonClientRedisson.create(config);Stringphonephone_987654321;ListStringidCardsList.of(idCard_123456,idCard_654321);booleansuccesstryBookTicket(redissonClient,phone,idCards);System.out.println(抢票结果: (success?成功:失败));redissonClient.shutdown();}}3、关于redis-cluster的报错RedisException: CROSSSLOT Keys in request don’t hash to the same slot1优化1在 Redis 集群中槽位是通过对键进行哈希计算得到的。以把多个键合并成一个键这样就能保证所有键都被哈希到同一个槽位。也可以使用{}包裹来确定hash槽-- 合并后的键localcombinedKey..KEYS[1]..:..KEYS[2]-- 检查 idCard 和 phone 是否存在localidCardExistsredis.call(HEXISTS,combinedKey,idcard)localphoneExistsredis.call(HEXISTS,combinedKey,phone)ifidCardExists0andphoneExists0then-- 设置 idCard 和 phoneredis.call(HSET,combinedKey,idcard,1)redis.call(HSET,combinedKey,phone,1)return1elsereturn0end注意这里的键分到不同的槽是传过来的参数key而不是实际执行命令的最终key所以传的参数key是需要使用括号将需要hash的部分括起来的。ListObjectkeysArrays.asList({8061}:8061,{8061}:291X);Stringlualocal combinedKey KEYS[1] .. : .. KEYS[2]\nlocal idCardExists redis.call(HEXISTS, combinedKey, idcard)\nlocal phoneExists redis.call(HEXISTS, combinedKey, phone)\nif idCardExists 0 and phoneExists 0 then\n redis.call(HSET, combinedKey, idcard, 1)\n redis.call(HSET, combinedKey, phone, 1)\n return 1\nelse\n return 0\nend \n;ObjectevalredissonClient.getScript().eval(RScript.Mode.READ_WRITE,lua,RScript.ReturnType.INTEGER,keys);System.out.println(eval);2优化2// 需要将所有的key都用{}包起来ListObjectkeysArrays.asList({8061}:291X,{8061}:8061);Stringlualocal idCardKey repeat:idcard: .. KEYS[1] \nlocal phoneKey repeat:phone: .. KEYS[2] \nlocal idCardExists redis.call(EXISTS, idCardKey) \nlocal phoneExists redis.call(EXISTS, phoneKey) \nif idCardExists 0 and phoneExists 0 then \n redis.call(SET, idCardKey, 1) \n redis.call(SET, phoneKey, 1) \n return 1 \nelse \n return 0 \nend;ObjectevalredissonClient.getScript().eval(RScript.Mode.READ_WRITE,lua,RScript.ReturnType.INTEGER,keys);System.out.println(eval);returnok;三、常用lua1、判断手机号和身份证号不重复我现在要写一个抢票的逻辑使用java的redisson保证每个身份证和手机号都只能抢一次帮我写一个lua脚本来判断身份证和手机号是否抢过-- 获取传入的参数localidCardKeyKEYS[1]localphoneKeyKEYS[2]-- 检查身份证和手机号对应的键是否存在localidCardExistsredis.call(EXISTS,idCardKey)localphoneExistsredis.call(EXISTS,phoneKey)-- 如果身份证和手机号对应的键都不存在则设置为已抢票状态ifidCardExists0andphoneExists0thenredis.call(SET,idCardKey,1)redis.call(SET,phoneKey,1)return1elsereturn0end实际过程中手机号会传入一个身份证号会传入多个不确定几个重新改一下lua脚本-- 获取手机号对应的键localphoneKeyKEYS[1]-- 获取身份证号的数量localidCardCount#KEYS-1-- 检查手机号对应的键是否存在localphoneExistsredis.call(EXISTS,phoneKey)ifphoneExists1thenreturn0end-- 检查每个身份证号对应的键是否存在fori2,#KEYSdolocalidCardKeyKEYS[i]localidCardExistsredis.call(EXISTS,idCardKey)ifidCardExists1thenreturn0endend-- 如果手机号和所有身份证号对应的键都不存在则设置为已抢票状态redis.call(SET,phoneKey,1)fori2,#KEYSdolocalidCardKeyKEYS[i]redis.call(SET,idCardKey,1)endreturn12、实现分布式锁importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.core.script.DefaultRedisScript;importorg.springframework.stereotype.Component;importjavax.annotation.Resource;importjava.util.Collections;importjava.util.concurrent.TimeUnit;ComponentpublicclassRedisDistributedLock{ResourceprivateRedisTemplateString,StringredisTemplate;// 锁前缀privatestaticfinalStringLOCK_PREFIXredis:lock:;// 默认锁过期时间 30秒privatestaticfinallongDEFAULT_EXPIRE30L;// 默认获取锁等待时间 10秒privatestaticfinallongDEFAULT_WAIT_TIME10L;/** * 获取分布式锁基础版 * param lockKey 锁key * param requestId 唯一标识用于防误删 * param expireTime 过期时间秒 * return 是否获取成功 */publicbooleanlock(StringlockKey,StringrequestId,longexpireTime){// 核心setIfAbsent Redis SETNX 命令 不存在则设置BooleansuccessredisTemplate.opsForValue().setIfAbsent(LOCK_PREFIXlockKey,requestId,expireTime,TimeUnit.SECONDS);returnBoolean.TRUE.equals(success);}/** * 释放分布式锁必须用Lua脚本保证原子性 * param lockKey 锁key * param requestId 唯一标识 * return 是否释放成功 */publicbooleanunlock(StringlockKey,StringrequestId){// Lua脚本判断锁归属再删除原子操作Stringscriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;DefaultRedisScriptLongredisScriptnewDefaultRedisScript();redisScript.setScriptText(script);redisScript.setResultType(Long.class);// 执行脚本LongresultredisTemplate.execute(redisScript,Collections.singletonList(LOCK_PREFIXlockKey),requestId);returnLong.valueOf(1).equals(result);}/** * 阻塞式获取锁循环尝试带超时 */publicbooleantryLock(StringlockKey,StringrequestId){returntryLock(lockKey,requestId,DEFAULT_WAIT_TIME,DEFAULT_EXPIRE);}publicbooleantryLock(StringlockKey,StringrequestId,longwaitTime,longexpireTime){longendTimeSystem.currentTimeMillis()waitTime*1000;// 循环尝试获取锁while(System.currentTimeMillis()endTime){if(lock(lockKey,requestId,expireTime)){returntrue;}// 短暂休眠避免CPU空转try{Thread.sleep(100);}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}returnfalse;}}importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjavax.annotation.Resource;importjava.util.UUID;RestControllerpublicclassTestController{ResourceprivateRedisDistributedLockdistributedLock;GetMapping(/test)publicStringtestLock(){StringlockKeyorder:create;// 生成唯一标识StringrequestIdUUID.randomUUID().toString();booleanlockSuccessfalse;try{// 尝试获取锁等待10s锁过期30slockSuccessdistributedLock.tryLock(lockKey,requestId,10,30);if(!lockSuccess){return获取锁失败当前请求繁忙;}// 执行业务逻辑 System.out.println(获取锁成功执行业务...);Thread.sleep(5000);// 模拟业务耗时return业务执行完成;}catch(Exceptione){e.printStackTrace();return业务异常;}finally{// 必须在finally中释放锁if(lockSuccess){distributedLock.unlock(lockKey,requestId);System.out.println(释放锁成功);}}}}