一.分布式锁
1.1 为什么要使用分布式锁
例如一个简单的用户操作,一个线程去修改用户的状态,首先从数据库中读出用户的状态,然后在内存中进行修改,修改完成后,再存回去。 如果是在单线程中,这个操作没有问题的。 如果是在多线程中,在我们进行修改的时候。先读取数据,再修改数据,最后存取数据,这是三个操作并不是一步完成的,所以在多线程中,这样做就有问题了。
1.2 分布式锁基本用法
分布式锁实现的思路很简单,就是进来一个线城先占位,当别的线城进来操作时,发现已经有人占位了,就会放弃或者稍后再试。
注意:分布式锁操作中 ,我们一般使用Redis的setnx指令 ,先进来的线程先占位 ,线程的操作执行完成后,再去调用del指令去释放所占用的位置。
方法一: 用普通多线程的思想去解决这个问题
实际操作:
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
Long setnx = jedis.setnx("k1", "v1");
if (setnx == 1) {
jedis.set("name", "javaboy");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else{
}
});
}
}
方法二: 解决方法一中的抛出异常和线程挂掉的问题
注意: 上面这样操作是有问题的。
如果业务执行过程中要抛异常或者挂了,这样就会导致del指令没有被调用,这样的话,k1就无法被释放,导致后来的请求会全部的堵塞在这里,锁没办法被释放。
问题解决的办法 :给这个锁去设置一个过期的时间 ,确保锁在一定时间后一定会被释放 ,以防止抛出异常或者直接挂掉所导致的锁被占用。改进后的代码如下:
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
Long setnx = jedis.setnx("k1", "v1");
if (setnx == 1) {
jedis.expire("k1", 5);
jedis.set("name", "javaboy");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else{
}
});
}
}
方法三:解决方法二中的服务器挂掉所导致的锁没办法释放的问题
注意 :这么写还是会有问题,就是在获取锁和设置过期时间之间如果服务器突然挂掉了,还是会导致锁被占用无法及时得到释放,还是会造成死锁,因为获取锁和设置过期时间其实是两个操作,不具备原子性。 为了解决这个问题,从Redis2.8 开始,setnx和expire 可以通过一个命令来一起执行 了,所以我们再进行改进一下。
public class LockTest {
public static void main(String[] args) {
Redis redis = new Redis();
redis.execute(jedis->{
String set = jedis.set("k1", "v1", new SetParams().nx().ex(5));
if (set !=null && "OK".equals(set)) {
jedis.expire("k1", 5);
jedis.set("name", "javaboy");
String name = jedis.get("name");
System.out.println(name);
jedis.del("k1");
}else{
}
});
}
}
方法四:解决上述方法中遗留的设置锁时间的问题
注意: 在方法二中,为了去解决业务代码在执行的时候抛出异常,我们给每一个锁通过jedis.expire("k1", 5); 设置一个超时时间去设置了一个超时时间,即,当锁被占用的时间超时了之后,锁会被自动释放,例如:在此处我们设置的超时时间是5s,超过了5s,这个锁就被自动释放了。
但是这也带来了一个新问题 :如果要执行的业务非常耗时,可能会出现紊乱。 举个例子:第一个线程首先获取到锁,然后去执行业务代码,但是我们第一个线程的业务非常麻烦,花费了8s,但是当执行到第五秒 不就超时了吗,就会自动去释放第一个线程锁占用的锁。然后在第五秒 的时候第二个线程进来占位使用了,可是当第八秒 的时候,第一个线程执行完毕了,此时第一个线程就会被释放,就会去执行释放锁的操作,但是他不是没有锁了吗,他就去释放了第二个线程的锁,释放完了之后第三个线程又进来了,冤种线程们就重复执行上面的步骤,越弄越乱。 那么面对这种问题我们要怎么去解决?
一般来说有两种方法:
方法1.尽量避免在获取锁之后,执行耗时操作。 方法2.可以在锁上做文章,将锁的value设置为一个随机的字符串,这样当每次释放锁的时候,都去比骄傲随机的字符串是否相同,如果相同,就再去执行释放的流程,不如不相同就不用释放。
对于方法2,由于释放锁的时候,要去第一步要去查看锁的value,第二步要去比较value的值是否正确,第三步释放锁。这三个步骤,但是这三个步骤明显不具备原子性,为了解决这个问题,我们去引入Lua脚本。
Lua脚本的优势:
优势1:使用方便 ,Redis中内置了对Lua脚本的支持。 优势2:Lua脚本可以在Redis服务端原子的去执行多 个Redis命令 。 优势3:由于网络 在很大程度上会影响到Redis的性能,而使用Lua脚本可以让多个命令一次被执行,可以有效的解决网络给Redis造成的性能的问题。
为什么要使用Lua脚本 :尽管Redis在6的时候已经默认使用多线程了,但是本质最核心的还是单线程 来进行操作的,就会出现很多问题,但是如果我们在Redis中使用Lua脚本的话,Redis就默认Lua脚本中一系列的操作为一个原子操作。(Redis中默认支持了Lua脚本的支持,我们可以直接使用)
在Redis中,使用Lua脚本,主要以两种思路: 1.提前在Redis服务端i下好Lua脚本,然后在java客户端去调用脚本(这里我们推荐这种方法) 2.可以直接在Java端去写Lua的脚本,写好后需要执行的时候每次将脚本发送到Redis上去执行。
本次Lua脚本内容如下:
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
解释 :
第一行解释: if redis.call(“get”,KEYS[1])==ARGV[1] then 首先redis.call 表示redis要去执行接下来的命令 其次 (“get”,KEYS[1])==ARGV[1] 表示等下我java代码去调用这个Lua脚本的时候要去传递两种参数,第一组参数是keys,第二组参数叫ARGV,而KEYS[1]指的是我等下传的KEYS里面的第一个参数,即等下Redis执行这一段Lua脚本我们JAVA要去传递三个参数,第一个参数是要执行的Lua脚本的参数,第二个参数是KEYS的list集合,第三个参数是ARGV的list集合,1就是访问第一个
linux上操作的步骤
1.创一个Redis中创一个lua文件夹 2.进入创一个xxx.lua 文件 3.vi进入拷贝入我们上面的代码
退到redis路径下去执行
src/redis-cli -a 123 -x script load < lua/mylua.lua lua/mylua.lua 解释 lua是我的文件夹 mylua.lua是我的lua文件的名字,你的看着更改
java中调用脚本即可
public static void main(String[] args) {
new Redis().execute(jedis -> {
String value = UUID.randomUUID().toString();
String result = jedis.set("k1", value, new SetParams().nx().ex(10L));
if ("OK".equals(result)) {
jedis.set("name", "zhangsan");
try {
Thread.sleep(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
jedis.evalsha("c2ee3882740fd0eff9dc0125fa36eb206831cf94", Arrays.asList("k1"), Arrays.asList(value));
} else {
}
});
}
}
|