redis实现分布式锁
1.什么是分布式锁?
1.1 情景
假设,对一个商品表的简单操作,一个线程去修改这个商品的信息,首先得从数据库中查出这条数据,然后加载在内存中,在内存修改完成之后,再存到数据库里面,对于单线程而言这一个完整的操作是没问题的,但是在多线程中,由于读取,修改,保存到数据库不是原子操作(原子特性:不可分割,要么全都成功,要么全都失败),所以这种操作出现在多线程中就会有很大的问题。
1.2 用redis实现分布式锁的思路?
分布式锁的思路不难,其实就是先进来的线程先占位置,当其他线程进来时,发现位置被人占了,就会放弃争夺或者稍后再来争夺。
在redis中,用setnx命令和del命令,可以简单的实现分布式锁,setnx命令返回1就代表线程抢到锁了,setnx命令返回0代表线程没抢到锁,del命令可以释放锁
2. 简单实现
2.1 jedisutils
package com.yl;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class JedisUtils {
private static JedisPool jedisPool = null;
public static Jedis getJedisObject() {
if (jedisPool == null) {
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
//最大空闲数
config.setMaxIdle(400);
//最大连接数
config.setMaxTotal(2000);
//连接最大等待时间,-1代表没有限制
config.setMaxWaitMillis(300000);
/**
* 配置连接池的地址,端口号,超时时间,密码
*/
jedisPool = new JedisPool(config,"192.168.244.129",6379,30000,"root123");
}
try {
//通过连接池获取jedis对象
Jedis jedis = jedisPool.getResource();
jedis.auth("root123");
return jedis;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
2.1 测试
package com.yl;
import redis.clients.jedis.Jedis;
public class LockTest {
public static void main(String[] args) {
Jedis jedis = JedisUtils.getJedisObject();
Long setnx = jedis.setnx("k1", "v1");
if (setnx == 1) {
//没人占位
//有可能没有执行到del那一步,所以给锁添加过期时间
jedis.expire("k1",5);
jedis.set("name", "yl");
String name = jedis.get("name");
System.out.println(name);
//释放资源
jedis.del("k1");
} else {
//有人占位,停止操作
System.out.println("没有获取到锁!");
}
}
}
3. (2.0简单实现分布锁)存在的问题以及解决方案
3.1 存在的问题
从2.0可以看到,获取到锁后,要设置过期时间(设置过期时间是预防业务代码出错,锁一直没释放掉!),那么这里会出现一种情况,假设这个过期时间设得比较小,然后,线程一进来了,执行它自己的业务代码,很复杂,执行得很慢,花费的时间远大于锁的超时时间,这个时候,线程一还在执行自己的业务代码,线程二抢到了锁进来了,然后也执行自己的业务代码,执行到一半,线程一的业务代码执行完了,线程二的还没执行完,线程一按道理还是会去释放锁的,这个时候线程二是锁的拥有者,线程一释放掉线程二的锁,这种情况合理?显然不合理,2.0中锁的value都固定为v1,所以它们公用同一个锁,且它们的value都是一样的!
3.2 解决方案:锁的value用随机字符串来替代再结合lua脚本判断传入的key去redis中查到的value和传入的value是否一致,如果一致就del掉key,否则直接返回
3.2.1 进入到redis文件,创建脚本文件
mkdir lua
cd lua/
vi release.lua
3.2.2 release.lua脚本文件内容
if redis.call("get",KEYS[1])==ARGV[1]then
return redis.call("del",KEYS[1])
else
return 0
end
3.2.3 求出SHA1和,作用:将lua脚本文件加载到redis缓存中,并且返回一个参数,在调用lua脚本时要传这个参数
cat lua/release.lua | redis-cli -a root123 script load --pipe
3.2.4 测试
package com.yl;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.SetParams;
import java.util.Arrays;
import java.util.UUID;
public class LockTest3 {
public static void main(String[] args) {
Jedis jedis = JedisUtils.getJedisObject();
//1.先随机获取一个字符串
String str= UUID.randomUUID().toString();
//2.获取锁,并且设置过期时间为5秒
String lock = jedis.set("k1", str, new SetParams().nx().ex(5));
//3.是否拿到锁
if (lock != null && "OK".equals(lock)) {
//成功拿到锁
//4.具体业务写这里
jedis.set("name","yl");
jedis.set("age","23");
System.out.println(jedis.get("name"));
System.out.println(jedis.get("age"));
//5.调用lua脚本,释放自己的锁
jedis.evalsha("b8059ba43af6ffe8bed3db65bac35d452f8115d8", Arrays.asList("k1"),Arrays.asList(str));
} else {
//没获取到锁
System.out.println("没有获取到锁!");
}
}
}