redis缓存 实现分布式锁

 

 解决方案

在数据库中查询没有 为null  那么将这个查询字段作为key,
value 为null 存储到redis 中 并为这个key 加入过期时间

------------------------------------------------------------------

 

 

 

 

 

 

 

 

 

 

 

 

 

 

复制代码
缓存没有命中.....
------->查询数据库操作.
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
------->查询数据库操作
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
缓存没有命中.....
------->查询数据库操作


但是此时发现 查询数据库操作输出了多次数据库查询
这是因为在 锁住代码块的同时 其他请求也在访问,但是锁里面的代码还在运行 这期间是获取不到数据的
就会一直出现查询数据库期间 一起出现缓存没有命中的情况
当去数据库查询完成返回数据的时候 且返回的数据不为空
但是在向缓存中写入数据的同时 其他数据已经抢到 之前释放的锁了 然后继续查询数据库操作 所以才会出现以上情况
释放锁和查询缓存结果时序问题

如果product 服务有多个  当请求打在相同的服务 不同的节点的时候 本地锁无法做到

 

 

 针对本地锁的问题  我们需要通过分布式锁来解决 但是不能说本地锁就没用了

 

 我们需要结合本地锁和分布式锁来解决 并发量大的情况下 确保分布式锁的压力较小 且访问数据库的操作 安全 不被其他线程篡改

如果大量访问直接打到分布式锁,分布式锁难以支撑这么多的并发   所以需要用本地锁 只放一个线程抢到锁  

多个相同服务 都只放一个请求去抢分布式的锁  那么就大大降低了分布式锁的压力

 

复制代码
复制代码
分布式锁的原理

 

 .分布式锁的常用解决方案

可以利用MySQL隔离性:唯一索引
```

```sql
use test;
CREATE TABLE `DistributedLock` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁名',
  `desc` varchar(1024) NOT NULL DEFAULT '备注信息',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uidx_name` (`name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法';

//数据库中的每一条记录就是一把锁,利用的mysql唯一索引的排他性

lock(name,desc){
    insert into DistributedLock(`name`,`desc`) values (#{name},#{desc});
}

unlock(name){
    delete from DistributedLock where name = #{name}
}
```

可以利用拍他说来实现 select .... where ...  for update;

乐观锁:乐观的任务数据不会出现数据安全问题,如果出现了就重试一次

```sql
select ...,version;
update table set version+1 where version = xxx
```

### 2.2 Redis

setNX: setNX(key,value) :如果key不存在那么就添加key的值,否则添加失败,Redisson

### 2.3Zookeeper
zookeeper 可以创建临时有序节点 并且可以相互监听
判断当前节点是不是整个节点中最小的
如果是 获取锁
当该节点使用完毕 释放锁 删除该节点
因为其他节点会监听节点的删除事件 然后判断002是否是所有节点中最小的
如果是最小的 获取锁
如果当前节点突然下先 那么其他节点就会监听比他们小的节点

 

 

复制代码
复制代码
 /**
* 根据父编号 获取对应的子菜单信息
* @param list
* @param parentCid
* @return
*/
private List<CategoryEntity> queryByParentCid(List<CategoryEntity> list,Long parentCid){
List<CategoryEntity> collect = list.stream().filter(item -> {
return item.getParentCid().equals(parentCid);
}).collect(Collectors.toList());
return collect;
}

/* //本地缓存
// private Map<String,Map<String,List<Catalog2VO>>> cache=new HashMap<>();*/


/**
* 查询出所有的二级和三级分类的数据
* 并封装为Map<String, Catalog2VO>对象
* @return
*/
@Override
public Map<String, List<Catalog2VO>> getCatelog2JSON() {
String key = "catalogJSON";
// Redis中获取分类的信息
String catalogJSON = stringRedisTemplate.opsForValue().get(key);
if(StringUtils.isEmpty(catalogJSON)){
System.out.println("缓存没有命中.....");
// 缓存中没有数据,需要从数据库中查询
Map<String, List<Catalog2VO>> catelog2JSONForDb = getCatelog2JSONDbWithRedis();

return catelog2JSONForDb;
}
System.out.println("缓存命中了....");
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
Map<String, List<Catalog2VO>> stringListMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2VO>>>() {
});
return stringListMap;
}


public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedis() {
String keys = "catalogJSON";
String uuid = UUID.randomUUID().toString();
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
System.out.println("获取分布式锁成功....");
Map<String, List<Catalog2VO>> data = null;
try {
data = getDataForDB(keys);
} catch (Exception e) {
e.printStackTrace();
} finally {
String srctip="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";//定义redis 删除脚本
//通过redis lua脚本实现查询和删除的原子性
stringRedisTemplate.execute(new DefaultRedisScript<Long>(srctip,Long.class)
,Arrays.asList("lock"),uuid);
}
return data;
}else {

try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("获取锁失败.....");
return getCatelog2JSONDbWithRedis();
}

}

/* public Map<String, List<Catalog2VO>> getCatelog2JSONDbWithRedisXXXXXXXXXXXXXXX() {
String keys = "catalogJSON";
//加锁 在执行插入操作的同时设置了过期时间相当于 setNX+ttl
String uuid = UUID.randomUUID().toString();
//如果设置30秒 操作数据库还是没有结束 第二个请求过来 设置了可以 当你业务完成后删除key 时可能删除的是别人的key---->设置查询锁的标识UUID来区分
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
//给对应的key设置过期时间 即使出现异常 到了10秒后也会将key删除 但是如果在expire之前就中断了 那么还是会造成死锁的问题----》这时我们希望setNxexpire 之间保证原子性 相当于setNx
// stringRedisTemplate.expire("lock", 10,TimeUnit.SECONDS);
//加锁成功 如果这里查询数据库出现异常的话 就会抛出异常下面的代码不会去执行 那么就会造成死锁
Map<String, List<Catalog2VO>> data = null;
try {
data = getDataForDB(keys);
} catch (Exception e) {
e.printStackTrace();
} finally {
String srctip="if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
"then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";//定义redis 删除脚本
//通过redis lua脚本实现查询和删除的原子性
stringRedisTemplate.execute(new DefaultRedisScript<Integer>(srctip,Integer.class)
,Arrays.asList("lock"),uuid);
}

//获取当前key 对应的值
String val = stringRedisTemplate.opsForValue().get("lock");//redis 官网推荐通过脚本删除指定锁的key 而不是del命令
//如果在查询和删除之间 不是原子性操作 就会出现查询来的key 时间过期了 key被删除了 其他请求创建一个新的key 原来的执行 删除了这个key 这时我们需要保证查询和删除时原子性
if(uuid.equalsIgnoreCase(val)){
//如果相等 说明没有请求覆盖掉自己的key 是自己的key
//从数据库中获取数据后 我们要释放锁
stringRedisTemplate.delete("lock");
}

return data;
}else {
//加锁失败
//休眠+重试
//Thread
return getCatelog2JSONDbWithRedis();
}
//释放锁
}*/
/**
* 从数据库中查询操作
* @param keys
* @return
*/
private Map<String, List<Catalog2VO>> getDataForDB(String keys) {
// 先去缓存中查询有没有数据,如果有就返回,否则查询数据库
// Redis中获取分类的信息
//这里为什么又要获取一次redis缓存的数据 因为有可能
String catalogJSON = stringRedisTemplate.opsForValue().get(keys);
if (!StringUtils.isEmpty(catalogJSON)) {
// 说明缓存命中
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
Map<String, List<Catalog2VO>> stringListMap = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catalog2VO>>>() {
});
return stringListMap;
}
System.out.println("-----------》查询数据库操作");

// 获取所有的分类数据
List<CategoryEntity> list = baseMapper.selectList(new QueryWrapper<CategoryEntity>());
// 获取所有的一级分类的数据
List<CategoryEntity> leve1Category = this.queryByParentCid(list, 0l);
// 把一级分类的数据转换为Map容器 key就是一级分类的编号, value就是一级分类对应的二级分类的数据
Map<String, List<Catalog2VO>> map = leve1Category.stream().collect(Collectors.toMap(
key -> key.getCatId().toString()
, value -> {
// 根据一级分类的编号,查询出对应的二级分类的数据
List<CategoryEntity> l2Catalogs = this.queryByParentCid(list, value.getCatId());
List<Catalog2VO> Catalog2VOs = null;
if (l2Catalogs != null) {
Catalog2VOs = l2Catalogs.stream().map(l2 -> {
// 需要把查询出来的二级分类的数据填充到对应的Catelog2VO
Catalog2VO catalog2VO = new Catalog2VO(l2.getParentCid().toString(), null, l2.getCatId().toString(), l2.getName());
// 根据二级分类的数据找到对应的三级分类的信息
List<CategoryEntity> l3Catelogs = this.queryByParentCid(list, l2.getCatId());
if (l3Catelogs != null) {
// 获取到的二级分类对应的三级分类的数据
List<Catalog2VO.Catalog3VO> catalog3VOS = l3Catelogs.stream().map(l3 -> {
Catalog2VO.Catalog3VO catalog3VO = new Catalog2VO.Catalog3VO(l3.getParentCid().toString(), l3.getCatId().toString(), l3.getName());
return catalog3VO;
}).collect(Collectors.toList());
// 三级分类关联二级分类
catalog2VO.setCatalog3List(catalog3VOS);
}
return catalog2VO;
}).collect(Collectors.toList());
}

return Catalog2VOs;
}
));
// 从数据库中获取到了对应的信息 然后在缓存中也存储一份信息
//cache.put("getCatelog2JSON",map);
// 表示缓存命中了数据,那么从缓存中获取信息,然后返回
if (map == null) {
//如果查询的数据数据库中不存在
// 那就说明数据库中也不存在 防止缓存穿透
stringRedisTemplate.opsForValue().set(keys, "1", 5, TimeUnit.SECONDS);
} else {
// 从数据库中查询到的数据,我们需要给缓存中也存储一份
// 防止缓存雪崩 将查询出的数据转换成json 数据 存入redis 并设置过期时间解决缓存雪崩
String json = JSON.toJSONString(map);
stringRedisTemplate.opsForValue().set("catalogJSON", json, 10, TimeUnit.MINUTES);
}
return map;
}
复制代码

 

posted @   花心大萝卜li  阅读(26)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示