文章目录

  • 版本 1.0 本地锁
    • 原生场景
    • 缓存
    • 缓存击穿
  • 版本 2.0 分布式锁
    • 基本原理
    • SETNX
    • 分布式锁-阶段 1
    • 分布式锁-阶段 2
    • 分布式锁-阶段 3
    • 分布式锁-阶段 4
    • 分布式锁-阶段 5

版本 1.0 本地锁

原生场景

进入一个网站首页,首页向后端发送请求,后端向数据库请求几百个数据的商品分类信息。

向数据库查询分类信息

// 向数据库查询分类信息
String getCatalogJsonFromDB(){String res = null;/*get res from db*/return res;
}

缓存

考虑到网站点击量较高,且商品分类请求方法需要查询数据库,加上数据封装,时间较长,所以要将查询到的数据缓存到 redis 中。忽略数据格式的转化。

String getCatalogJsonFromRedis(){// 尝试向 redis 获取数据String catalogJson = redisTemplate.opsForValue().get("catalogJson");if(StringUtils.isEmpty(catalogJson)){// redis 中没有,向数据库查String catalogJsonFromDB = getCatalogJsonFromDB();// 写入 redisredisTemplate.opsForValue().set("catalogJson", catalogJsonFromDB);return catalogJsonFromDB;}return catalogJson;
}

逻辑也很简单,后端直接向 redis 请求数据,如果不存在的话,再向数据库请求,且把数据库请求得来的数据放到 redis 中。

缓存击穿

相关介绍

作为一个网站的首页,访问量较高,虽设置了缓存,但是缓存有过期时间,过期的时刻,本来由 redis 承担的请求全部打在数据库上,容易造成数据库的宕机。

解决方案:使用互斥锁,只让一个请求去访问数据库。

重写 getCatalogJsonFromDB 方法

String getCatalogJsonFromDB(){synchronized (this){String catalogJson = redisTemplate.opsForValue().get("catalogJson");// 双重校验if(!StringUtils.isEmpty(catalogJson)){return catalogJson;}String res = null;/*get res from db*/return res;}
}

使用 synchronized (this) 的原因是,springboot 生成的对象都是单例模式,可以认为锁到所有访问对象。双重校验,如果前面有线程查询了数据库且向 redis 存放了数据,可以再从 redis 返回,不需要查库。

预想中是一次查库,其余查缓存。

问题: 线程 A、B 发来请求,redis 中数据为空,进入到 getCatalogJsonFromDB 方法。线程 A 争夺到锁,进入代码块,从数据库查询到数据并返回,释放锁之后,向 redis 中存储数据。在这个过程里,线程 B 得到锁, redis 中数据还没存进去,于是继续查库。降低了系统性能

可以将写入缓存这一操作放在释放锁之前

String getCatalogJsonFromDBAndCache(){synchronized (this){String catalogJson = redisTemplate.opsForValue().get("catalogJson");// 双重校验if(!StringUtils.isEmpty(catalogJson)){return catalogJson;}String res = null;/*get res from db*/redisTemplate.opsForValue().set("catalogJson", res);return res;}
}

这样可以实现一次查库,其余查缓存。前提是单体服务。

这里主要利用 synchronized (this) 实现加锁功能,之前说这样可行是因为在一个 springboot 微服务里,每个对象都是单例模式的,所以锁 this 相当于锁所有请求。

但是如果分布式呢?

这里复制 3 个相同的服务,同时启动,使用 JMeter 向网关发送请求。

结果是启动的四个微服务里,有三个查询了数据库,意味着有三个也写了缓存。


这样难免会造成分布式的微服务下数据的不一致性。

版本 2.0 分布式锁

基本原理

虽然服务是分布式的,但是目前来看, redis 只有一个,所以只需要给 redis 加锁,即可实现只有一个微服务下的一个线程向 redis 存储数据。

SETNX

SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • EX:设置键key的过期时间,单位秒
  • PX:设置键key的过期时间,单位毫秒
  • NX:只有键key不存在的时候才会设置key的值
  • XX:只有键key存在的时候才会设置key的值

NX 这个参数引导,每个微服务下的线程竞争设置一个为 “lock” 的 key,只有一个可以设置成功。

分布式锁-阶段 1

不再使用本地锁,将上述 getCatalogJsonFromDBAndCache去除本地锁。

private String getCatalogJsonFromDbWithRedisLock() {// 去 redis 加锁Boolean successful = redisTemplate.opsForValue().setIfAbsent("lock", "lock");if(successful){// 设置过期时间redisTemplate.expire("lock", 30, TimeUnit.SECONDS);// 加锁成功 执行业务String res = getCatalogJsonFromDbAndCache();// 执行完 删除锁redisTemplate.delete("lock");return res;}else{// 加锁失败 自旋try {Thread.sleep(200);} catch (InterruptedException e) {}return getCatalogJsonFromDbWithRedisLock();}
}
  • 如果不删除锁?其他线程阻塞,一直自旋,导致死锁
  • 如果不设置过期时间?删除锁之前停电/异常,无法删除,导致死锁

分布式锁-阶段 2

加锁的时候同时设置过期时间,必须保证原子性

Boolean successful = redisTemplate.opsForValue().setIfAbsent("lock", "lock", 300, TimeUnit.SECONDS);

删锁的时候锁已经过期的话,其他线程已经获取锁,设置了自己的 lock,此时再删锁其实删的是别人的锁,多个线程会同时访问。

分布式锁-阶段 3

设置 key-value 时创建一个独特的 UUID ,删除时候判断是否是自己的锁。如果是,才删除。

private String getCatalogJsonFromDbWithRedisLock() {// 去 redis 加锁String uuid = UUID.randomUUID().toString();Boolean successful = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);if(successful){// 设置过期时间// 加锁成功 执行业务String res = getCatalogJsonFromDbAndCache();String lock = redisTemplate.opsForValue().get("lock");if(uuid.equals(lock)){// 执行完 删除锁redisTemplate.delete("lock");}return res;}else{// 加锁失败 自旋try {Thread.sleep(200);} catch (InterruptedException e) {}return getCatalogJsonFromDbWithRedisLock();}
}

问题也相当明显,判断锁和删锁也不是原子性的,也会出现误删的情况。判断的时候是自己的锁,然后锁到期,其他线程占锁,删除了别人的。

分布式锁-阶段 4

官方文档建议使用 lua 脚本执行原子性操作

if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end

最终版本

private String getCatalogJsonFromDbWithRedisLock() {String uuid = UUID.randomUUID().toString();// 原子加锁Boolean successful = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);if(successful){String res = null;try {// 加锁成功 执行业务res = getCatalogJsonFromDbAndCache();} catch (Exception e) {// lua 脚本String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";// 原子解锁redisTemplate.execute(new DefaultRedisScript<Long>(script),Arrays.asList("lock"),uuid);}return res;}else{// 加锁失败 自旋try {Thread.sleep(200);} catch (InterruptedException e) {}return getCatalogJsonFromDbWithRedisLock();}
}

只有一个微服务中的一次获取分布式锁成功

分布式锁-阶段 5

Redisson 实现

Redisson 相关内容

private String getCatalogJsonFromDbWithRedissonLock(){RLock lock = redissonClient.getLock("catalogJSON-lock");lock.lock();String catalogJsonFromDb;try {catalogJsonFromDb = getCatalogJsonFromDbAndCache();} finally {lock.unlock();}return catalogJsonFromDb;
}

缓存与分布式锁——场景实现相关推荐

  1. Day140-142.尚品汇:AOP+Redis缓存+redssion分布式锁、CompletableFuture异步编排、首页三级分类展示、Nginx静态代理

    目录 Day08 一.获取商品详情 加入缓存 二.全局缓存:分布式锁与aop 整合 三.布隆过滤器 四.CompletableFuture 异步编排 jdk1.8 Day09 1. 将item 改为多 ...

  2. 数据结构(字典,跳跃表)、使用场景(计数器、缓存、查找表、消息队列、会话缓存、分布式锁)、Redis 与 Memcached、 键的过期时间、数据淘汰策略、持久化(RDB、AOF)

    1. 数据结构 1.1 字典 dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry /* This is our hash table structure. Every dic ...

  3. redis应用场景—— 缓存,分布式锁,去重

    Redis实际应用场景 https://www.cnblogs.com/mrhgw/p/6278619.html Redis在很多方面与其他数据库解决方案不同: 它使用内存提供主存储支持,而仅使用硬盘 ...

  4. Mall商城的高级篇的开发(三)缓存与分布式锁

    缓存 在程序中,缓存是一个高速数据存储层,其中存储了数据子集,且通常是短暂性存储,这样日后再次请求此数据时,速度要比访问数据的主存储位置快.通过缓存,可以高效地重用之前检索或计算的数据. 为什么要使用 ...

  5. Redis缓存——(分布式锁)

    目录 分布式缓存 缓存击穿,穿透,雪崩 分布式锁 Redisson实现分布式锁 Lock锁 读写锁 分布式缓存 对于本地模式下的缓存,每次如果负载均衡请求的服务器不相同,那么会有很大的几率不通过缓存, ...

  6. Redis实现 分布式缓存、分布式锁

    缓存逻辑 代码实例 堆外内存溢出 springboot2.0以后默认使用lettuce作为操作rendis的客户端,在高并发下回产生"堆外内存溢出",可以通过切换使用jedis. ...

  7. 分布式锁的3种实现(数据库、缓存[redis]、Zookeeper)

    锁是开发过程中十分常见的工具,在处理高并发请求的时候和订单数据的时候往往需要锁来帮助我们保证数据的安全. 一.分布式锁的场景 场景1.前端点击太快,导致后端重复调用接口.两次调用一个接口,这样就会产生 ...

  8. 并发场景下的幂等问题——分布式锁详解

    简介:本文从钉钉实人认证场景的一例数据重复问题出发,分析了其原因是因为并发导致幂等失效,引出幂等的概念.针对并发场景下的幂等问题,提出了一种实现幂等可行的方法论,结合通讯录加人业务场景对数据库幂等问题 ...

  9. 22-09-20 西安 谷粒商城(04)Redisson做分布式锁、布隆过滤器、AOP赋能、自定义注解做缓存管理、秒杀测试

    Redisson 1.Redisson做分布式锁  分布式锁主流的实现方案: 基于数据库实现分布式锁 基于缓存(Redis),性能最高 基于Zookeeper,可靠性最高 Redisson是一个在Re ...

最新文章

  1. 关于poll机制应用及驱动
  2. R语言数据结构之矩阵
  3. 整理:C++中sprintf()函数的使用详解
  4. 计算机系统中存储管理是,《计算机操作系统5、存储管理.doc
  5. webpack初体验_使用webpack打包js文件_json文件_使用webpack开发模式_生产模式打包---webpack工作笔记003
  6. 1.1.2获取和控制线程状态(Getting and Seeting Thread State)
  7. 银行卡号识别(CTPN、Densenet、CTC)附数据集
  8. 【英语四六级-必背单词】高中英语单词 (H)-MP3试听与下载
  9. 服务器系统https打不开网页,解决网站启动HTTPS出现重定向过多网页打不开问题...
  10. [日推荐]『Brick4积木』乐高迷不可错过的小工具
  11. 益智app游戏 android,儿童宝宝益智游戏
  12. 耐得住寂寞,是一种境界和品味
  13. 3.多边形曲线简化之Douglas-Peucker算法
  14. 生命的质量,在于你拥有多少内在的和平与喜悦
  15. OpenCV Python 人脸识别签到系统(超详细注释)
  16. 【时间之外】区块链和BT的技术是孪生的吗?
  17. 转行做数据分析的心路历程
  18. 论文解读:ChangeFormer | A TRANSFORMER-BASED SIAMESE NETWORK FOR CHANGE DETECTION
  19. Android组件间数据传递
  20. 杰里AC695N开发详解汇总(持续更新中)

热门文章

  1. 去除IOS升级提示小红点
  2. python实现在图片上画框并标注
  3. 赠书活动环节抽签C++程序
  4. 共享场地预约小程序系统软件开发方案
  5. 微信小程序云开发学习
  6. LeetCode【每日一题】 1095. 山脉数组中查找目标值
  7. 学计算机怎么入校,计算机摇号入学到底是如何进行随机录取的?一文了解!
  8. Android Studio 安装和配置genymotion模拟器过程中所遇到的问题
  9. 制造企业发展第三方物流的思路探讨 (zt)
  10. 计量经济学Stata-自用笔记