锁是用来解决什么问题的;java
在单进程中,咱们能够用到synchronized、lock之类的同步操做去解决,可是对于分布式架构下多进程的状况下,如何作到跨进程的锁。就须要借助一些第三方手段来完成linux
分布式锁的解决方案redis
lock(数据库
id int(11)apache
methodName varchar(100),缓存
memo varchar(1000)安全
modifyTime timestamp服务器
unique key mn (method) --惟一约束网络
)多线程
获取锁的伪代码
try{
exec insert into lock(methodName,memo) values(‘method’,’desc’); method
return true;
}Catch(DuplicateException e){
return false;
}
释放锁
delete from lock where methodName=’’;
利用zookeeper的惟一节点特性或者有序临时节点特性得到最小节点做为锁. zookeeper 的实现相对简单,经过curator客户端,已经对锁的操做进行了封装,原理以下
1. 可靠性高、实现简单
2. zookeeper由于临时节点的特性,若是由于其余客户端由于异常和zookeeper链接中断了,那么节点会被删除,意味着锁会被自动释放
3. zookeeper自己提供了一套很好的集群方案,比较稳定
4. 释放锁操做,会有watch通知机制,也就是服务器端会主动发送消息给客户端这个锁已经被释放了
redis中有一个setNx命令,这个命令只有在key不存在的状况下为key设置值。因此能够利用这个特性来实现分布式锁的操做
释放锁的代码
linux的内核会把全部外部设备都看做一个文件来操做,对一个文件的读写操做会调用内核提供的系统命令,返回一个 file descriptor(文件描述符)。对于一个socket的读写也会有响应的描述符,称为socketfd(socket 描述符)。而IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好之后就通知该进程
IO多路复用又称为事件驱动,操做系统提供了一个功能,当某个socket可读或者可写的时候,它会给一个通知。当配合非阻塞socket使用时,只有当系统通知我哪一个描述符可读了,我才去执行read操做,能够保证每次read都能读到有效数据。操做系统的功能经过select/pool/epoll/kqueue之类的系统调用函数来使用,这些函数能够同时监视多个描述符的读写就绪状况,这样多个描述符的I/O操做都能在一个线程内并发交替完成,这就叫I/O多路复用,这里的复用指的是同一个线程
多路复用的优点在于用户能够在一个线程内同时处理多个socket的 io请求。达到同一个线程同时处理多个IO请求的目的。而在同步阻塞模型中,必须经过多线程的方式才能达到目的
Lua是一个高效的轻量级脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能
到官网下载lua的tar.gz的源码包
tar -zxvf lua-5.3.0.tar.gz
进入解压的目录:
cd lua-5.2.0
make linux (linux环境下编译)
make install
若是报错,说找不到readline/readline.h, 能够经过yum命令安装
yum -y install readline-devel ncurses-devel
安装完之后再make linux / make install
最后,直接输入 lua命令便可进入lua的控制台
在Lua脚本中调用Redis命令,可使用redis.call函数调用。好比咱们调用string类型的命令
redis.call(‘set’,’hello’,’world’)
redis.call 函数的返回值就是redis命令的执行结果。前面咱们介绍过redis的5中类型的数据返回的值的类型也都不同。redis.call函数会将这5种类型的返回值转化对应的Lua的数据类型
在不少状况下咱们都须要脚本能够有返回值,在脚本中可使用return 语句将值返回给redis客户端,经过return语句来执行,若是没有执行return,默认返回为nil。
Redis提供了EVAL命令可使开发者像调用其余Redis内置命令同样调用脚本。
[EVAL] [脚本内容] [key参数的数量] [key …] [arg …]
能够经过key和arg这两个参数向脚本中传递数据,他们的值能够在脚本中分别使用KEYS和ARGV 这两个类型的全局变量访问。好比咱们经过脚本实现一个set命令,经过在redis客户端中调用,那么执行的语句是:
lua脚本的内容为: return redis.call(‘set’,KEYS[1],ARGV[1]) //KEYS和ARGV必须大写
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world
EVAL命令是根据 key参数的数量-也就是上面例子中的1来将后面全部参数分别存入脚本中KEYS和ARGV两个表类型的全局变量。当脚本不须要任何参数时也不能省略这个参数。若是没有参数则为0
eval "return redis.call(‘get’,’hello’)" 0
考虑到咱们经过eval执行lua脚本,脚本比较长的状况下,每次调用脚本都须要把整个脚本传给redis,比较占用带宽。为了解决这个问题,redis提供了EVALSHA命令容许开发者经过脚本内容的SHA1摘要来执行脚本。该命令的用法和EVAL同样,只不过是将脚本内容替换成脚本内容的SHA1摘要
经过如下案例来演示EVALSHA命令的效果
script load "return redis.call('get','hello')" 将脚本加入缓存并生成sha1命令
evalsha "a5a402e90df3eaeca2ff03d56d99982e05cf6574" 0
咱们在调用eval命令以前,先执行evalsha命令,若是提示脚本不存在,则再调用eval命令
实现一个针对某个手机号的访问频次, 如下是lua脚本,保存为phone_limit.lua
local num=redis.call('incr',KEYS[1])
if tonumber(num)==1 then
redis.call('expire',KEYS[1],ARGV[1])
return 1
elseif tonumber(num)>tonumber(ARGV[2]) then
return 0
else
return 1
end
经过以下命令调用
./redis-cli --eval phone_limit.lua rate.limiting:13700000000 , 10 3
语法为 ./redis-cli –eval [lua脚本] [key…]空格,空格[args…]
redis的脚本执行是原子的,即脚本执行期间Redis不会执行其余命令。全部的命令必须等待脚本执行完之后才能执行。为了防止某个脚本执行时间过程致使Redis没法提供服务。Redis提供了lua-time-limit参数限制脚本的最长运行时间。默认是5秒钟。
当脚本运行时间超过这个限制后,Redis将开始接受其余命令但不会执行(以确保脚本的原子性),而是返回BUSY的错误
打开两个客户端窗口
在第一个窗口中执行lua脚本的死循环
eval “while true do end” 0
在第二个窗口中运行get hello
最后第二个窗口的运行结果是Busy, 能够经过script kill命令终止正在执行的脚本。若是当前执行的lua脚本对redis的数据进行了修改,好比(set)操做,那么script kill命令没办法终止脚本的运行,由于要保证lua脚本的原子性。若是执行一部分终止了,就违背了这一个原则
在这种状况下,只能经过 shutdown nosave命令强行终止
java代码
RedisManager.java
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class RedisManager { private static JedisPool jedisPool; static { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(20); jedisPoolConfig.setMaxIdle(10); jedisPool = new JedisPool(jedisPoolConfig, "120.79.174.118", 6379); } public static Jedis getJedis() throws Exception { if (null != jedisPool) { return jedisPool.getResource(); } throw new Exception("Jedispool was not init"); } }
RedisLock.java 简单实现分布式锁
import java.util.List; import java.util.UUID; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class RedisLock { public String getLock(String key, int timeout) { try { Jedis jedis = RedisManager.getJedis(); String value = UUID.randomUUID().toString(); long end = System.currentTimeMillis() + timeout; while (System.currentTimeMillis() < end) { if (jedis.setnx(key, value) == 1) { // 锁设置成功,redis操做成功 jedis.expire(key, timeout); return value; } if (jedis.ttl(key) == -1) { // 检测过时时间,没有设置则设置 jedis.expire(key, timeout); } Thread.sleep(1000); } } catch (Exception e) { e.printStackTrace(); } return null; } public boolean releaseLock(String key, String value) { try { Jedis jedis = RedisManager.getJedis(); while (true) { jedis.watch(key);// watch if (value.equals(jedis.get(key))) {// 判断得到锁的线程和当前redis中存的锁是同一个 Transaction transaction = jedis.multi(); transaction.del(key); List<Object> list = transaction.exec(); if (list == null) { continue; } return true; } jedis.unwatch(); break; } } catch (Exception e) { e.printStackTrace(); } return false; } public static void main(String[] args) { String key = "aaa"; RedisLock redisLock = new RedisLock(); String lockId = redisLock.getLock(key, 10000); if (null != lockId) { System.out.println("得到锁成功"); } else { System.out.println("得到锁失败"); } String lockId2 = redisLock.getLock(key, 10000); if (null != lockId2) { System.out.println("得到锁成功"); } else { System.out.println("得到锁失败"); } boolean ret = redisLock.releaseLock(key, lockId); if (ret) { System.out.println("释放锁成功"); } else { System.out.println("释放锁失败"); } String lockId3 = redisLock.getLock(key, 10000); if (null != lockId3) { System.out.println("得到锁成功"); } else { System.out.println("得到锁失败"); } boolean ret2 = redisLock.releaseLock(key, lockId3); if (ret2) { System.out.println("释放锁成功"); } else { System.out.println("释放锁失败"); } } }
LuaDemo.java 执行lua脚本
import java.util.ArrayList; import java.util.List; import redis.clients.jedis.Jedis; public class LuaDemo { public static void main(String[] args) throws Exception { Jedis jedis = RedisManager.getJedis(); String lua="local num=redis.call('incr',KEYS[1])\n"+ "if tonumber(num)==1 then\n"+ " redis.call('expire',KEYS[1],ARGV[1])\n"+ " return 1\n"+ "elseif tonumber(num)>tonumber(ARGV[2]) then\n"+ " return 0\n"+ "else\n"+ " return 1\n"+ "end"; List<String> keys=new ArrayList<>(); keys.add("ip:limit:127.0.0.1"); List<String> arggs=new ArrayList<>(); arggs.add("6000"); arggs.add("10"); Object obj=jedis.eval(lua,keys,arggs); System.out.println(obj); } }
LuaDemo2.java 经过sha摘要缓存lua脚本
import java.util.ArrayList; import java.util.List; import redis.clients.jedis.Jedis; public class LuaDemo2 { public static void main(String[] args) throws Exception { Jedis jedis = RedisManager.getJedis(); String lua="local num=redis.call('incr',KEYS[1])\n"+ "if tonumber(num)==1 then\n"+ " redis.call('expire',KEYS[1],ARGV[1])\n"+ " return 1\n"+ "elseif tonumber(num)>tonumber(ARGV[2]) then\n"+ " return 0\n"+ "else\n"+ " return 1\n"+ "end"; List<String> keys = new ArrayList<>(); keys.add("ip:limit:127.0.0.1"); List<String> arggs = new ArrayList<>(); arggs.add("6000"); arggs.add("10"); // 经过sha摘要缓存lua脚本,减小网络传输,提升性能。(redis重启缓存的sha摘要会丢失) String sha = jedis.scriptLoad(lua); System.out.println(sha); Object obj = jedis.evalsha(sha, keys, arggs); System.out.println(obj); } }
maven配置
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <version>2.4.3</version> </dependency>