1.原理
分布式锁的一个基本原理就是多个服务内的多个线程访问某资源时,全都到同一个地方占坑,这个地方就是缓存,被所有服务共享的缓存,第一个访问的线程会去缓存中设置
一个key value 缓存,由于是原子性的操作,后面的线程设置同样key时缓存失败,从而实现一个分布式的锁
2.linux命令实现方式
指令参考
http://www.redis.cn/commands/set.html
从上链接可以看到当给set指令添加NX选项时,只有当key不存在时才能缓存该数据
(1)复制四个链接
(2)发送同一条命令连接到客户端(记得先切root权限)
(3)连接上后再次同时发送缓存命令
连接1
连接2,3,4全部是如下界面,都返回nil 也就是null
可以看到NX缓存模式下,key值相同时,只有第一次缓存会成功 这就是分布式锁的基本原理
3.代码实现方式与完善优化
初步实现,核心代码如下,就是在我们一个线程访问进行(查询数据库并保存到数据到缓存)时,给其加上分布式锁,也就是在redis中以NX模式设置一个key为lock的缓存,当其它线程访问时,若设置缓存失败也就是获取分布式锁失败,进行自旋,也就是重复的调用本方法再次进行尝试,当然为了尝试不要太频繁可设置休眠时间
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
//先从缓存中尝试获取,若为空则从数据库中获取然后放入缓存
String catalogJson;
synchronized (this) {
ValueOperations<String, String> opsForValue = stringRedisTemplate.opsForValue();
catalogJson = opsForValue.get("catalogJson");
System.out.println("从缓存内查询...");
if (StringUtils.isEmpty(catalogJson)) {
Map<String, List<Catelog2Vo>> catalogJsonFromDb = null;
try {
catalogJsonFromDb = getCatalogJsonFromDbWithRedisLock();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("从数据库内查询...");
return catalogJsonFromDb;
}
}
Map<String, List<Catelog2Vo>> catalogJsonFromCache = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return catalogJsonFromCache;
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() throws InterruptedException {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111");
if(lock){
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
//删除锁
stringRedisTemplate.delete("lock");
return dataFromDb;
}else{
Thread.sleep(100);
return getCatalogJsonFromDbWithRedisLock();
}
}
分析下以上实现存在的问题,删除锁之前出现异常或者断电,宕机等情况时会造成死锁
,所以我们给该key(锁)设置一个过期时间
这次由于我们是分开设置的,如果获取锁之后设置过期时间之前断电啥的,也会导致死锁
解决思路:缓存与缓存数据的过期时间设置必须为原子性的,redis支持,所以改为以下代码
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",30,TimeUnit.SECONDS);
可是还有问题,当我们查询数据库并添加到缓存或者处理业务逻辑时间过长,超过我们设置的过期时间,此时如果逻辑执行完后删除锁就是删除的别的线程的了
核心代码,值设置为uuid来方便删除时识别是否为当前线程的锁
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() throws InterruptedException {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",30,TimeUnit.SECONDS);
String uuid = UUID.randomUUID().toString();
if(lock){
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
//删除锁 如果是当前线程的才删除 不然可能删的是线程的锁
String value = stringRedisTemplate.opsForValue().get("lock");
if(uuid.equals(value)){
stringRedisTemplate.delete("lock");
}
return dataFromDb;
}else{
Thread.sleep(100);
return getCatalogJsonFromDbWithRedisLock();
}
}
可是还有问题,由于获取value对比与删除缓存不是原子性操作,获取value之后与删除缓存之前万一宕机停电啥的,也会导致删除别人的锁,相当于没锁住
所以再次对代码进行改造,用redis提供的脚本删锁方式来保证原子性
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() throws InterruptedException {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
String uuid = UUID.randomUUID().toString();
if(lock){
Map<String, List<Catelog2Vo>> dataFromDb;
try {
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
//保证获取value对比与删除缓存是原子操作,这里采用redis提供的执行脚本方式
Long lock1 = stringRedisTemplate.execute(new DefaultRedisScript<Long>(), Arrays.asList("lock"), uuid);
}
return dataFromDb;
}else{
Thread.sleep(200);
return getCatalogJsonFromDbWithRedisLock();
}
}
此时就演进成了阶段五最终形态
至此就基本已经实现并完善了分布式锁,压测这里我就不做了,结果是可以预想的会成功
由于代码的重复性,可以将其封装,当然这些别人都已经做好了,后面将会了解分布式锁更专业的框架--Redisson
本篇主要参考链接: