1.分布式锁是什么
分布式锁是控制分布式系统或不同系统之间共同访问共享资源的一种锁实现
如果不同的系统或同一个系统的不同主机之间共享了某个资源时,往往通过互斥来防止彼此
干扰。
2.分布锁设计目的
可以保证在分布式部署的应用集群中,同一方法在同一操作只能被同一台机器上的同一个线程执行
3.分布式锁的场景
Redis里面存入了一部分缓存数据,缓存的数据是来自db,我们把数据从db刷到缓存中,有时候就需要一个定时任务来做这个同步操作,在做这个操作的时候,我们必须控制只有一台机来做这个同步操作,如果是多台机来做这个操作,那么就会造成资源的浪费以及不必要的干扰。如下图所示,同一份代码是分布式部署,那么定时任务就是三份,同时去将数据库的数据刷到redis中,这时候三个定时任务之间就会产生干扰或者数据的紊乱,那么这时候分布式锁就派上用场了。
4.设计要求
这把锁要是一把可重⼊锁(避免死锁)
这把锁有高可用的获取锁和释放锁功能
这把锁获取锁和释放锁的性能要好…
5.分布锁实现方案案分析
1、获取锁的时候,使用SETNX key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1; 若 key 存在,则什么都不做,返回 【0】加锁,锁的 value 值为当前占有锁服务器内网IP编号拼接任务标识。在释放锁的时候进行判断。并使用 expire 命令为锁添 加一个超时时间,超过该时间则自动释放锁。 这一点非常重要,因为程序正常执行,会进入finally去释放锁,但如果服务器突然宕机,执行不到finally,就一直被锁死了,所以需要设置超时时间进行双重保险
2、获取锁的时候调用setnx,如果返回0,则锁正在被别人使用,返回1则成功获取锁。还要设置一个获取的超时时间,超过这个时间则自动放弃获取锁。
3、释放锁的时候,判断是不是该锁(即Value为当前服务器内⽹IP编号拼接任务标识。若是该锁,则执⾏ delete 。
解锁的操作就要引入try catch finally来完成,就算代码有return,finally也一定会先执行再去执行return操作,保证锁的释放
6.代码开发
下面就是代码开发,注释我都放上去了,流程与上面讲的一致
/**
* 定时任务
*/
@Service
public class RedisLockJob {
private static final Logger logger = LoggerFactory.getLogger(RedisLockJob.class);
private static String LOCK_PREFIX = "prefix_";
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisClient redisClient;
@Scheduled(cron = "0/5 * * * * *")//每隔5秒跑一次,秒分时日月年
public void lock(){
String lock = LOCK_PREFIX + "RedisLockJob";
try {
//redistemplate setnx操作
boolean nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
//获取锁失败
if(!nxRet){
String value = (String)redisClient.get(lock);
//打印当前占用锁的服务器IP
logger.info("get lock fail,lock belong to:{}",value);
return;
//获取锁成功,则设置超时时间,防止服务器宕机被锁死
}else{
redisTemplate.opsForValue().set(lock,getHostIp(),3600);
//执行具体业务,下面随便写,都是想实现的业务
logger.info("start lock lockNxExJob success");
Thread.sleep(5000);
}
}catch (Exception e){
logger.error("lock error",e);
}finally {
redisClient.remove(lock);
}
System.out.println("enter job"+System.currentTimeMillis()/1000);
}
/**
* 获取本机内网IP地址方法
* @return
*/
private static String getHostIp(){
try{
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()){
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()){
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":")==-1){
return ip.getHostAddress();
}
}
}
}catch(Exception e){
e.printStackTrace();
}
return null;
}
}
7.测试
验证过程其实也比较简单,就是上面说的,同一个定时任务部署多份,只有一台机器执行了定时任务操作就表示成功了
将代码上传到102,103服务器,使用nohup java -jar jar包名称 & 命令进行后台启动,在启动目录会有nohup.out可以查看,由下图日志可以看出代码已经成功了
8.Redis分布式锁可能出现的问题
在生产环境中,可能会由于某些服务忽然请求压力增大,内存使用飚高等等情况,需要kill掉进程,这时候服务就挂掉了,这就是第一种问题server down.第二种就是redis服务挂了,redis出现雪崩等各种情况,这时候问题就比较大了,比如像如下代码,刚执行完setnx操作,还没有执行setex操作服务就挂了,那么就会导致这个key一直存在,永远不会过期,那么服务重启之后所有定时任务一直都获取不到锁,这时候只能就去redis数据库去手动删除key了。
9.解决方案
其实问题主要存在的就是setnx与setex两个命令中间可能会存在服务器宕机的问题,我们只需要解决怎么一次性执超过2条命令并不会出现问题。这里有如下解决方案(也可以使用redisconnect方式):
9.1采用Lua脚本
Lua简介:
1、从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使⽤ EVAL 命令对 Lua 脚本进⾏求值。
2、Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原⼦性(atomic) 的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effffect)要么是不可见的(not visible),要么就是已完成的(already completed)
使用Lua脚本的好处:
1、减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延。
2、原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
3、复用。客户端发送的脚本会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
Springboot配置Lua脚本配置流程:
1、在resource目录下新增一个后缀名为.lua结尾的文件
2、编写lua脚本
3、传入lua脚本的key和arg
4、调用redisTemplate.execute
这里我们就将上面的代码进行修改:
1、先在resource下面加一个add.lua的文件,里面编写脚本(具体语法可以网上搜一下,比较简单),主要目的就是实现setnx以及setex的原子性
2、将第六步代码修改如下(其实也就是新增一个调用lua脚本的方法):
@Service
public class LuaRedisLockJob {
private static final Logger logger = LoggerFactory.getLogger(LuaRedisLockJob.class);
@Autowired
private RedisClient redisClient;
@Autowired
private RedisTemplate redisTemplate;
private static String LOCK_PREFIX = "lua_";
private DefaultRedisScript<Boolean> lockScript;
@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
boolean luaRet = false;
try {
luaRet = luaExpress(lock,getHostIp());
//获取锁失败
if (!luaRet) {
String value = (String) redisClient.get(lock);
//打印当前占用锁的服务器IP
logger.info("lua get lock fail,lock belong to:{}", value);
return;
} else {
//获取锁成功
logger.info("lua start lock lockNxExJob success");
Thread.sleep(5000);
}
} catch (Exception e) {
logger.error("lock error", e);
} finally {
if (luaRet) {
logger.info("release lock success");
redisClient.remove(lock);
}
}
}
/**
* 获取lua结果
* @param key
* @param value
* @return
*/
public Boolean luaExpress(String key,String value) {
//拿到操作脚本的类
lockScript = new DefaultRedisScript<Boolean>();
//加载脚本
lockScript.setScriptSource(
new ResourceScriptSource(new ClassPathResource("add.lua")));
//设置返回结果
lockScript.setResultType(Boolean.class);
// 封装参数
List<Object> keyList = new ArrayList<Object>();
keyList.add(key);
keyList.add(value);
Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);
return result;
}
/**
* 获取本机内网IP地址方法
*
* @return
*/
private static String getHostIp() {
try {
Enumeration<NetworkInterface> allNetInterfaces = NetworkInterface.getNetworkInterfaces();
while (allNetInterfaces.hasMoreElements()) {
NetworkInterface netInterface = (NetworkInterface) allNetInterfaces.nextElement();
Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress ip = (InetAddress) addresses.nextElement();
if (ip != null
&& ip instanceof Inet4Address
&& !ip.isLoopbackAddress() //loopback地址即本机地址,IPv4的loopback范围是127.0.0.0 ~ 127.255.255.255
&& ip.getHostAddress().indexOf(":") == -1) {
return ip.getHostAddress();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}