Lua在Redis中的应用—分布式锁,限制访问次数
2018-01-08 11:00
447 查看
Lua在Redis中的应用—分布式锁,限制访问次数
Lua是一个高效的轻量级脚本语言。它是开源的,非常小巧,整个源码也才五百来K,可以很方便地嵌入到程序中(无论是桌面端还是移动端)1.分布式锁
分布式锁可以用多种方式来实现常用为以下方式:1、基于数据库表做乐观锁,用于分布式锁。
2、memcached
3、redis
4、zookeeper
我们本次只说一下redis(r2m)的实现方式,并由简单分布式锁,以及问题分析,逐渐改进分布式锁的问题。最终达到完成一个尽可能完美的解决方案。(完美是相对的,最终并不能解决集群中锁所在服务器redis进程崩溃,而引起的锁失效问题)。
1.1首先看下简单锁:
/** * 获取锁(简单) * @param lockName 锁名 * @param tryNum 重试次数 * @return * @throws InterruptedException */ private synchronized String acquire_lock(Jedis redis,String lockName,int tryNum) { String uuid =System.currentTimeMillis()+""; for (int i = 0; i < tryNum; i++) { Long n = redis.setnx("lock:"+lockName, uuid); if(n==1) { return uuid; } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } /** * 释放锁 * @param lockName 锁名 * @param lockValue 锁内容 */ private void release_lock(Jedis redis,String lockName,String lockValue) { String lockname = "lock:"+lockName; boolean chek = lockValue.equals(redis.get(lockname)); if(chek){ redis.del(lockname); } } @Test public void testRedisLock() throws InterruptedException { int thread_Num = 15; CountDownLatch countDownLatch = new CountDownLatch(thread_Num); String lockName = Thread.currentThread().getName(); for (int i = 0; i < thread_Num; i++) { executor.execute(new Runnable() { Jedis redis = RedisUtil.getJedis(); @Override public void run() { try { String lockValue = null; try { lockValue = acquire_lock(redis, lockName, 5); } finally { if (lockValue != null) { release_lock(redis, lockName, lockValue); } } } finally { countDownLatch.countDown(); RedisUtil.returnResource(redis); } } }); } countDownLatch.await(); executor.shutdown(); }
这个简单的锁有什么问题呢?当持有锁线程死掉了会发生什么?这时锁就不可能再释放,释放锁的线程已死。
so我以将以上简单的锁改为可自动释放的锁
1.2自动释放锁
/** * 当获持有线程崩溃时,自动释放 * @param lockName 锁名 * @param tryNum 重试次数 * @param lock_timeout 锁自动超时时间 (毫秒) * @return */ private synchronized String acquire_loca_with_timeout(Jedis redis, String lockName, int tryNum, long lock_timeout) { String uuid = (lock_timeout + System.currentTimeMillis())+""; String key = "lock:" + lockName; for (int i = 0; i < tryNum; i++) { Long n = redis.setnx(key, uuid); if (n == 1) { // redis.expire("lock:"+lockName,lock_timeout); return uuid; } else { String time = redis.get(key); if(time!=null) { long t = Long.valueOf(time); if (System.currentTimeMillis() > t ) { String oldTime = redis.getSet(key, uuid); if (System.currentTimeMillis() >Long.valueOf(oldTime)) { return uuid; } } } } try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } return null; } /** * 释放锁 * @param lockName 锁名 * @param lockValue 锁内容 */ private void release_lock_with_timeout(Jedis redis,String lockName,String lockValue) { String lockname = "lock:"+lockName; boolean chek = lockValue.equals(redis.get(lockname)); if(chek){ String time = redis.get(lockname); //只有在超时范围内才是自己的锁,否则可能锁已被其它线程获得 if(System.currentTimeMillis()<= Long.valueOf(time)) { redis.del(lockname); } } } @Test public void testRedisLock() throws InterruptedException { int thread_Num = 15; CountDownLatch countDownLatch = new CountDownLatch(thread_Num); String lockName = Thread.currentThread().getName(); for (int i = 0; i < thread_Num; i++) { executor.execute(new Runnable() { Jedis redis = RedisUtil.getJedis(); @Override public void run() { try { String lockValue = null; try { lockValue = acquire_loca_with_timeout(redis,lockName,5,1000L); } finally { if (lockValue != null) { release_lock_with_timeout(redis,lockName,lockValue); } } } finally { countDownLatch.countDown(); RedisUtil.returnResource(redis); } } }); } countDownLatch.await(); executor.shutdown(); }
这一种实现有什么问题呢?这种实现比上种有很大的改善,它获取锁不再单纯依赖setnx
其中保存的value 修改为(当前时间+过期时间)
lock_timeout + System.currentTimeMillis()
String time = redis.get(key); if(time!=null) { long t = Long.valueOf(time); if (System.currentTimeMillis() > t ) { String oldTime = redis.getSet(key, uuid); if (System.currentTimeMillis() >Long.valueOf(oldTime)) { return uuid; } } }
如果setnx为0是看当前时间 是否大于保存的value如果是说明过期了,此时正常应该是直接就获取到锁了。但如果此时两个线程都获取过这个过期信号,so此时
redis.getSet(key, uuid)执行getset命令将先设置一个时间,并返回老的时间,再对比一次与当前时间的值大小,就可以避免这种情况了。但这时问题来了此时未获取到的线程也会修改这个key的时间(getset) 但这个影响不大。那有没有更好的解决办法呢:
1.3 set nx px实现锁
/** * set nx px 进行设置锁 * nx当不存在时才设置 xx为存在时才设置,px为毫秒 ex为秒 * 这个有什么坏处呢?: * 1、删除锁时如果这个锁已过期了页,而过期期间锁已被其它线程拿到,之后当前线程处理完了,del锁时已经删除的不是自己的锁了。 * 如下:A客户端拿到对象锁,但在因为一些原因被阻塞导致无法及时释放锁。 因为过期时间已到,Redis中的锁对象被删除。 B客户端请求获取锁成功。 A客户端此时阻塞操作完成,删除key释放锁。 C客户端请求获取锁成功。 这时B、C都拿到了锁,因此分布式锁失效。 * 2、要避免1中的情况发生,就要保证key的值是唯一的,且每一个拿到该key锁的值不一样,只有拿到锁的客户端才能进行删除。 * 基于这个原因,普通的del命令是不能满足要求的,我们需要一个能判断客户端传过来的value和锁对象的value是否一样的命令。Redis并没有这样的原子命令,这时可以通过Lua脚本来完成: * @param redis * @param lockName * @param tryNum * @param lock_timeout * @return */ public boolean acquire_loca_nxpx(Jedis redis, String lockName,String value, int tryNum, int lock_timeout) { for (int i = 0; i < tryNum; i++) { String valuel = redis.set(lockName, value, "NX", "PX", lock_timeout); if ("OK".equals(valuel)) { return true; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } return false; } /** * 释放锁 * @param redis * @param lockName * @param value * @return */ private long del_lock(Jedis redis, String lockName,String value) { String script = "local key =KEYS[1]; local value = ARGV[1] \n" + " if redis.call(\"get\",key) == value then \n" + " return redis.call(\"del\",key)\n" + " else \n" + " return 0 \n" + " end"; Object result = redis.eval(script, 1, lockName,value); return (long)result ; } @Test public void testLuaRedisLock() throws InterruptedException { int thread_Num = 5; CountDownLatch countDownLatch = new CountDownLatch(thread_Num); String lockName = Thread.currentThread().getName(); for (int i = 0; i < thread_Num; i++) { executor.execute(new Runnable() { Jedis redis = RedisUtil.getJedis(); @Override public void run() { try { boolean lockValue = false; String value = UUID.randomUUID().toString(); try { lockValue = acquire_loca_lua(redis,lockName,value,5,3000); } finally { if (lockValue) { del_lock(redis,lockName,value); } } } finally { countDownLatch.countDown(); RedisUtil.returnResource(redis); } } }); } countDownLatch.await(); executor.shutdown(); }
也可以将上同set nx px 改为lua 是一样的
/** * lua的一个获取锁的方法 效果与相同当然弊端也一样; * @see #acquire_loca_nxpx(Jedis, String, String, int, int) * @param redis * @param lockName * @param value * @param tryNum * @param lock_timeout * @return */ public boolean acquire_loca_lua(Jedis redis, String lockName, String value, int tryNum, int lock_timeout) { String script = "local key = KEYS[1] \n" + " local value = ARGV[1] \n" + " local outTime = ARGV[2] \n" + " local num = redis.call(\"setnx\",key,value)" + " if num == 1 then \n" + " redis.call(\"expire\",key,outTime) \n"+ " end \n" + " return num "; for (int i = 0; i < tryNum; i++) { Long num = (Long) redis.eval(script, 1, lockName,value,lock_timeout+""); System.err.println("acquire_loca_lua:"+num); if(num.intValue()==1) { return true; } try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } return false; }
至此锁就完了。。
2.IP防问次数限制
通过上面的例子我们也可以发现,只要有判断的,其实在分布式的系统中就已不再是原子操作,就算是在本地程序中加了锁,也只能保证在本JVM下的线程安全,但往往现在有服务都在多服务器部署。SOjava中的多并发大部分是用在处理数据上,而且往往不能多服务同时执行,除非从逻辑上进行分配数据如通过hash各服务器处理不同的数据 或者通过分布式锁等方法。redis的天生单线程和单进程。如果能将一些简单逻辑操作做为原子操作进行一块执行,就可以很方便的实现多服务器的原子操作。这可以通过redis的事务实现。但r2m(京东自研的分布式redis集群)并不支持事务,而且r2m的文档也说明了,所有需要事务的地方推荐使用lua脚本来实现。只要能事务实现的都可以用lua脚本实现。如下这个业务可以简单的用lua很方便的实现。
/** * @param redis * @param ip 限制的IP * @param limit_time 在某一个时间段(秒) 如:10 秒限制3次 ip_limit(xxx,"127.0.0.1",10,3) * @param limit_count 在这个时间内限制访问多少次 * @return */ private String ip_limit(Jedis redis,String ip,int limit_time,int limit_count) { String script = "local times = redis.call('incr',KEYS[1]) \n"+ " if times == 1 then \n" + "redis.call('expire',KEYS[1],ARGV[1]) \n" + "end \n" + "if times> tonumber(ARGV[2]) then \n" + " return 0 \n" + "end \n" + "return 1"; Object result = redis.eval(script, 1, ip,limit_time+"",limit_count+""); return (String) result; }
相关文章推荐
- 使用redis限制ip访问次数
- Java并发:分布式应用限流 Redis + Lua 实践
- PHP中Yii2框架用redis实现限制接口访问次数
- nginx lua redis 访问频率限制(转)
- PHP结合Redis来限制用户或者IP某个时间段内访问的次数
- PHP结合Redis来限制用户或者IP某个时间段内访问的次数
- 使用redis进行用户接口访问时间次数限制
- redis学习笔记---java操作redis,使用expire模拟指定时间段内限制ip访问的次数;
- nginx+lua 限制接口访问次数
- PHP实现redis限制单ip、单用户的访问次数功能
- java项目 Nginx+Lua+Redis ip次数限制 非集群
- 使用lua脚本编写访问次数限制
- [SpringMVC+redis]自定义aop注解实现控制器访问次数限制
- PHP实现redis限制单ip、单用户的访问次数功能示例
- springboot和redis控制单位时间内同个ip访问同个接口的次数
- nginx限制某个IP同一时间段的访问次数
- Lua语言模型 与 Redis应用
- nginx限制某个IP同一时间段的访问次数
- Nginx中如何限制某个IP同一时间段的访问次数
- Redis应用(3)客户端访问