redis setnx java_JAVA高并发之-Redis分布式锁setnx,setex连用

本文介绍了如何使用Redis实现分布式锁,通过Java操作Redis的setnx和setex命令,确保加锁和设置超时的原子性。讨论了在setnx和setex之间服务宕机导致的问题,并提出使用Lua脚本来一次性执行两个命令以避免此问题,同时展示了Lua脚本的配置和实战应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Redis分布式锁

* 分布锁满足两个条件,一个是加有效时间的锁,一个是高性能解锁

* 采用redis命令setnx(set if not exist)、setex(set expire value)实现

6805f316fa509cbc41e99f0b308e98b0.png

* 【千万记住】解锁流程不能遗漏,否则导致任务执行一次就永不过期

* 将加锁代码和任务逻辑放在try,catch代码块,将解锁流程放在finallypackage com.concurrent.schedule;

import com.concurrent.util.IpUtil;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.data.redis.core.StringRedisTemplate;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

import javax.management.MalformedObjectNameException;

import java.util.concurrent.TimeUnit;

/**

* Created by zhangjiawen on 2019/8/13.

*/

@Component

public class RedisSchedule {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired

private StringRedisTemplate redisTemplate;

private static final String publicKey="CommonKey";

Boolean lock=false;

@Scheduled(cron="*/5 * * * * *")

public void setNx() {

System.out.println("TimeUnit.SECONDS"+TimeUnit.SECONDS);

try {

String ipConfig=IpUtil.getIp() + ":" + IpUtil.getPort();

lock= redisTemplate.opsForValue().setIfAbsent(publicKey, ipConfig);

if(!lock){

String value=redisTemplate.opsForValue().get(publicKey);

logger.warn("key have exist belong to:"+value);

}else{

redisTemplate.opsForValue().set(publicKey,ipConfig,60, TimeUnit.SECONDS);

//获取锁成功

logger.info("start lock lockNxExJob success");

Thread.sleep(5000);

}

}catch (Exception e){

logger.error("setNx error!");

e.printStackTrace();

}finally {

redisTemplate.delete(publicKey);

}

}

}

分布式锁setnx、setex的缺陷,在setnx和setex中间发生了服务down机

* 从Redis宕机讲解分布式锁执行的异常场景流程

* 从Server服务宕机讲解分布式锁执行的异常场景流程

* 在setnx和setex中间发生了服务down机 那么key将没有超时时间 会一直存在,新的请求永远进不来

50fe9c25862d0af64be90c8f5dba1cb2.png

解决方案:

由于setnx与setex是分步进行,那么我们将两步合成一步,放在同一个原子中即可

* 怎么一次性执行过一条命令而不会出现问题,采用Lua脚本

* Redis从2.6之后支持setnx、setex连用

* Lua简介

* 从 Redis 2.6.0 版本开始,通过内置的 Lua 解释器,可以使用 EVAL 命令对 Lua 脚本进行求值。  * Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。* Lua脚本配置流程  * 1、在resource目录下面新增一个后缀名为.lua结尾的文件  * 2、编写lua脚本local lockKey = KEYS[1]

local lockTime = KEYS[2]

local lockValue = KEYS[3]

-- setnx info

local result_1 = redis.call('SETNX', lockKey, lockValue)

if result_1 == 1

then

local result_2= redis.call('SETEX', lockKey,lockTime, lockValue)

return result_2

else

return 'faild'

end

* 3、传入lua脚本的key和arg* 4、调用redisTemplate.execute方法执行脚本* Lua脚本结合RedisTempalte实战演练package com.concurrent.schedule;

import com.concurrent.util.IpUtil;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.core.io.ClassPathResource;

import org.springframework.data.redis.core.RedisTemplate;

import org.springframework.data.redis.core.script.DefaultRedisScript;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.scripting.support.ResourceScriptSource;

import org.springframework.stereotype.Component;

import java.util.ArrayList;

import java.util.List;

/**

* Created by zhangjiawen on 2019/8/13.

*/

@Component

public class LuaRedisSchedulePlus {

private Logger logger = LoggerFactory.getLogger(this.getClass());

@Autowired

private RedisTemplate redisTemplate;

private static final String publicKey="DEMO_COMMON_KEY";

Boolean lock=false;

private DefaultRedisScript lockScript;

@Scheduled(cron="*/5 * * * * *")

public void setNx() {

try {

String ipConfig=IpUtil.getIp() + ":" + IpUtil.getPort();

lock=  luaExpress(publicKey,"30",ipConfig);

if(!lock){

String value=(String) redisTemplate.opsForValue().get(publicKey);

logger.warn("key have exist belong to:"+value);

}else{

//获取锁成功

logger.info("start lock lockNxExJob success");

// Thread.sleep(5000);

}

}catch (Exception e){

logger.error("setNx error!");

e.printStackTrace();

}finally {

// redisTemplate.delete(publicKey);

}

}

/**

* 获取lua结果

* @param key

* @param value

* @return

*/

public Boolean luaExpress(String key,String  time,String value) {

lockScript = new DefaultRedisScript();

lockScript.setScriptSource(

new ResourceScriptSource(new ClassPathResource("add.lua")));

lockScript.setResultType(String.class);

// 封装参数

List keyList = new ArrayList();

keyList.add(key);

keyList.add(time);

keyList.add(value);

String result= (String)redisTemplate.execute(lockScript, keyList);

System.out.println(result);

logger.info("redis set result:"+result);

if (!"ok".equals(result.toLowerCase())){

return false;

}

return true;

}

}

* Lua脚本其他工作场景剖析和演练* lua eval http://doc.redisfans.com/script/eval.html

lua脚本做高可用分布式锁的优化

-当某个锁需要持有的时间小于锁超时时间时会出现两个进程同时执行任务的情况, 这时候如果进程没限制只有占有这把锁的人才能解锁的原则就会出现, A解了B的锁。

解决方案:删除key的时候判断一下value是否是当前value,是的话删除,否则不执行删除,将原来代码finally修改如下}catch (Exception e){

logger.error("setNx error!");

e.printStackTrace();

}finally {

// redisTemplate.delete(publicKey);

// releaseLock(publicKey,ipConfig);

}/**

* 释放锁操作

* @param key

* @param value

* @return

*/

private boolean releaseLock(String key, String value) {

lockScript = new DefaultRedisScript();

lockScript.setScriptSource(

new ResourceScriptSource(new ClassPathResource("unlock.lua")));

lockScript.setResultType(Boolean.class);

// 封装参数

List keyList = new ArrayList();

keyList.add(key);

keyList.add(value);

Boolean result = (Boolean) redisTemplate.execute(lockScript, keyList);

return result;

}

添加解锁lua脚本local lockKey = KEYS[1]

local lockValue = KEYS[2]

-- get key

local result_1 = redis.call('get', lockKey)

if result_1 == lockValue

then

local result_2= redis.call('del', lockKey)

return result_2

else

return false

end

技术之家希望本篇文章对你有用!!

欢迎来到技术之家,

如需转载,烦请保留本文链接和出处:http://www.jszja.com/contents/14/1453.html

您的支持将是我们前进的动力!如对本篇文章有疑问或建议,请通过本站下方邮箱联系我们,让技术之家每天进步一点点!(●'◡'●)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值