上一章说了在单体应用中加锁解决缓存击穿问题,但是在分布式中,每个服务会有很多个,如果使用本地锁,它只锁自己的服务,而不能实现在所有的服务中只查询一次数据库,所以在这种情况下,我们可以考虑使用分布式锁
基本原理
所有的服务都去一个公共的地方占锁,当一个服务拿到锁以后,他就可以执行相关的逻辑,而其他的服务就处于等待状态,这个公共的地方可以使MySQL,也可以是Redis,当然,我们的服务开发使用Redis做缓存,肯定是在Redis中加锁更加的方便,而Redis本身也提供对应的占锁的命令,详细请查看官方文档(传送门)
EX seconds-设置指定的到期时间,以秒为单位。
PX 毫秒-设置指定的到期时间(以毫秒为单位)。
NX -只有key不存在时设置key,相当于Redis中有锁,线程等待,我们在使用时就基于此
XX -只有key存在时设置key。
KEEPTTL -保留与key关联的生存时间。
GET-返回存储在key处的旧值;如果key不存在,则返回nil。
先来测试哈这个命令,可以看到,当我同时向Redis中set数据,并且使用NX,只有一个成功,这就是分布式锁的基本原理
用Java代码来表达
public void redisLock() throws InterruptedException {
// 通过NX方式存值,占分布式锁
Boolean flag = template.opsForValue().setIfAbsent("lock", "lock is not exist");
if (flag) {//占锁成功
//执行业务
System.out.println(template.opsForValue().get("lock"));
//业务执行成功后,需要删除锁给其他人用
template.delete("lock");
} else {//占锁失败,进行重试
TimeUnit.SECONDS.sleep(50);
redisLock();
}
}
这个锁相当于一个占位符,当服务拿到这个锁就可以进行后面的逻辑代码,而没有拿到锁的服务只能进行等待,这个锁是不参与实际业务的
但是这个锁就会衍生出一个问题,当我们的业务代码出现异常,而导致该进程结束而没有删除锁,那么锁将一直被占用,这就造成了死锁,这个问题可以通过设置过期时间来解决,在加锁时设置锁的过期时间,但是要保证这两个操作原子性
public void redisLock() throws InterruptedException {
// 通过NX方式存值,占分布式锁,设置过期时间防止死锁
Boolean flag = template.opsForValue().setIfAbsent("lock", "lock is not exist",30,TimeUnit.SECONDS);
if (flag) {//占锁成功
//执行业务
System.out.println(template.opsForValue().get("lock"));
//业务执行成功后,需要删除锁给其他人用
template.delete("lock");
} else {//占锁失败,进行重试
TimeUnit.SECONDS.sleep(50);
redisLock();
}
}
同样的在删除锁的还会出现问题,例如:业务逻辑执行时间较长,而锁的过期时间较短,当业务还没执行完锁已经过期,而其他的业务已经可以占锁了,当第一个业务逻辑执行完后进行删锁,就删除了是其他服务的锁。解决办法:给自己的服务指定uuid唯一值
public void redisLock() throws InterruptedException {
// 通过NX方式存值,占分布式锁,设置过期时间防止死锁
String uuid = UUID.randomUUID().toString();
Boolean flag = template.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (flag) {//占锁成功
//执行业务
String lock = template.opsForValue().get("lock");
System.out.println(lock);
//业务执行成功后,需要删除锁给其他人用
if (lock.equals(uuid)){//判断当前锁的值,防止误删其他服务的锁
template.delete("lock");
}
} else {//占锁失败,进行重试
TimeUnit.SECONDS.sleep(50);
redisLock();
}
}
在删锁的时候还有其它问题,当获取锁的值,由于网络传输的延时,在比对成功后还没执行删除之前,这个锁的就失效了,所以这个时候删除的锁依旧是其他服务的锁,这是有网络传输需要消耗时间造成的,这个需要使用lua脚本进行处理,Redis官方也是推荐给我们这么做,而我们的代码时是没办法直接处理的,所以最后就演变成下面的结果
public void redisLock() throws InterruptedException {
// 通过NX方式存值,占分布式锁,设置过期时间防止死锁
String uuid = UUID.randomUUID().toString();
Boolean flag = template.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
if (flag) {//占锁成功
try {
//执行业务
String lock = template.opsForValue().get("lock");
System.out.println(lock);
} finally {
//业务执行成功后,需要删除锁给其他人用
//lua脚本解锁
String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
//删除锁
template.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
} else {//占锁失败,进行重试
TimeUnit.SECONDS.sleep(50);
redisLock();
}
}
分布式锁还有更加强大专业的框架来处理,下一章就使用Redisson来解决这一些的问题