redis分布式锁

什么是分布式锁:

  分布式系统不同服务器访问共享资源造成数据的不一致,分布式锁只要是实现在分布式集群中始终只能有一台服务一个线程去修改访问数据.

 

使用场景:

 比如系统定时任务凌晨三点根据当天的用户任务完成数送相对应的金额,如果代码部署了多台服务器,在同一时间内会跑多次,造成必要的损失.

 

 

实现:

  在redis中使用简单的实现就是使用 setnx,setex, 先使用setnx,共用一个key,服务器的ip为value,如果存在就返回 0,不做任何操作,不存在设置成功,返回1, 返回 1后在使用 setex设置一个过期时间,最后程序运行完之后把key删除

 问题: 如果在设置完 setnx 后,服务器或者redis 宕机了,来不及设置过期时间和删除,在服务恢复后,这个key就会一直存在,除非手动删除

 

 代码案例:

@Component
public class LockNxExJob {

private static final Logger logger = LoggerFactory.getLogger(LockNxExJob.class);

@Autowired
private RedisTemplate redisTemplate;

private static String LOCK_PREFIX = "prefix_";


@Scheduled(cron = "0/10 * * * * *")
public void lockJob() {
String lock = LOCK_PREFIX + "LockNxExJob";
try{
//redistemplate setnx操作
boolean nxRet = redisTemplate.opsForValue().setIfAbsent(lock,getHostIp());
Object lockValue = redisService.get(lock);

//获取锁失败
if(!nxRet){
String value = (String)redisService.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 {
redisService.remove(lock);
}
}

/**
* 获取本机内网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;
}

public static void main(String[] args) {
String localIP = "";
try {
localIP = getHostIp();
} catch (Exception e) {
e.printStackTrace();
}
//获取本机IP
System.out.println(localIP);
}

}


2:解决方法:

使用 setnx 和 setex 连就不会出现这样的问题了,原子操作

方法1:使用 lua脚本,redis自身的运算能力并不强,在reids 2.6版本后支持了lua教程,lua脚本运算能力强,且是原子操作的,redis发送lua脚本的方式总共有两种,一种是直接把脚本发送的redis服务器那边运行,还用一种的话就是把脚本发送到redis服务器,然后redis会对脚本进行一个缓存,返回一个SHA1的32位的编码回来,

然后在去执行脚本只要把SHA1和参数发送redis就行了.为什么要用SHA1 32的编码呢,那是因为发送脚本到redis那边都是通过网络传输的,如果脚本过长,会存在着一定的延迟,这样的话网络传输会给redis的性能带来瓶颈,如果只发送编码和参数,这样的话就少发送了很多,速度也快了.


Spring为了支持redis的lua脚本,提供了一个 RedisScirpit的接口,还有一个DefaultRedisScirpit的接口实现类

 

 

String getSha1(); 获取SHA1返回的编码

Class<T> getResultType();  查看lua脚本返回值类型

String getScriptAsString();  查看编写的lua脚本字符串



使用redis lua脚本实现高可用分布式锁

加锁:

public Boolean luaLocak(String key, String value, Long expire) {

StringBuilder stringBuilder = new StringBuilder();
String s = stringBuilder.append("local result_1 = redis.call('SETNX', KEYS[1], ARGV[1])\n").append("if result_1 == true then \n")
.append(" local result_2= redis.call('SETEX', KEYS[1],ARGV[1], ARGV[2])) \n").append("return result_1 \n").append("else \n")
.append("return result_1 end \n").toString();

DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();

//设置返回值类型
defaultRedisScript.setResultType(Boolean.class);
//设置脚本
defaultRedisScript.setScriptText(s);

//设置Value值
List<String> list = new ArrayList<>();
list.add(key);

Boolean execute = stringRedisTemplate.execute(defaultRedisScript, list, value, String.valueOf(expire));

return execute;
}


解锁:
public Boolean luashi(String key, String value) {
StringBuilder dr = new StringBuilder();
dr.append("local lockKey = KEYS[1]\n").append("local lockValue = ARGV[1]\n").append("local result_1 = redis.call('get', lockKey)\n")
.append("if result_1 == lockValue then\n").append("local result_2= redis.call('del', lockKey)\n").append("return result_2\n")
.append("else\n").append("return false\n").append("end\n");

DefaultRedisScript<Boolean> rs = new DefaultRedisScript<>();
rs.setScriptText(dr.toString());
rs.setResultType(Boolean.class);

List<String> list = new ArrayList<>();
list.add(key);
Boolean execute = stringRedisTemplate.execute(rs, list, value);
return execute;

}


业务端:

 

Boolean boolen = jedisDistributedLock.luaLocak(key, "aaddd", (long) 500);
System.out.println(boolen);

try {
if (boolen) {
System.out.println("获取锁成功");
String s = stringRedisTemplate.opsForValue().get(key);
System.out.println(s);
} else {
System.out.println("获取锁失败");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (boolen) {
Boolean aaddd = jedisDistributedLock.luashi(key, "aaddd");
System.out.println("解锁状态:"+aaddd);
}

}


1:防止别的线程获取锁失败后把获取到锁的线程解锁,在 finally 和解锁里面做双重判断,只能获取到锁的才能解锁

2: lua 脚本中的 KEYS[1] ,KEYS[1] 代表的是客户端传的第一个键和第二个键,

 

 

 

 

args 代表的是客户端传递的 ARGV[1]  ARGV[2]  ........  逗号分隔传递

 

posted on 2020-04-13 14:20  散漫的烟灰  阅读(200)  评论(0编辑  收藏  举报