当前位置: 首页>关注 >
如何用ReadWriteLock实现一个通用的缓存中心? 微资讯
2023-05-30 15:23:03 来源:博客园
摘要:在并发场景中,Java SDK中提供了ReadWriteLock来满足读多写少的场景。
本文分享自华为云社区《【高并发】基于ReadWriteLock开了个一款高性能缓存》,作者:冰 河。
写在前面
在实际工作中,有一种非常普遍的并发场景:那就是读多写少的场景。在这种场景下,为了优化程序的性能,我们经常使用缓存来提高应用的访问性能。因为缓存非常适合使用在读多写少的场景中。而在并发场景中,Java SDK中提供了ReadWriteLock来满足读多写少的场景。本文我们就来说说使用ReadWriteLock如何实现一个通用的缓存中心。
本文涉及的知识点有:
【资料图】
读写锁
说起读写锁,相信小伙伴们并不陌生。总体来说,读写锁需要遵循以下原则:
- 一个共享变量允许同时被多个读线程读取到。
- 一个共享变量在同一时刻只能被一个写线程进行写操作。
- 一个共享变量在被写线程执行写操作时,此时这个共享变量不能被读线程执行读操作。
这里,需要小伙伴们注意的是:读写锁和互斥锁的一个重要的区别就是:读写锁允许多个线程同时读共享变量,而互斥锁不允许。所以,在高并发场景下,读写锁的性能要高于互斥锁。但是,读写锁的写操作是互斥的,也就是说,使用读写锁时,一个共享变量在被写线程执行写操作时,此时这个共享变量不能被读线程执行读操作。
读写锁支持公平模式和非公平模式,具体是在ReentrantReadWriteLock的构造方法中传递一个boolean类型的变量来控制。
public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this);}
另外,需要注意的一点是:在读写锁中,读锁调用newCondition()会抛出UnsupportedOperationException异常,也就是说:读锁不支持条件变量。
缓存实现
这里,我们使用ReadWriteLock快速实现一个缓存的通用工具类,总体代码如下所示。
public class ReadWriteLockCache{ private final Map m = new HashMap<>(); private final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 private final Lock r = rwl.readLock(); // 写锁 private final Lock w = rwl.writeLock(); // 读缓存 public V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 public V put(K key, V value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } }}
可以看到,在ReadWriteLockCache中,我们定义了两个泛型类型,K代表缓存的Key,V代表缓存的value。在ReadWriteLockCache类的内部,我们使用Map来缓存相应的数据,小伙伴都都知道HashMap并不是线程安全的类,所以,这里使用了读写锁来保证线程的安全性,例如,我们在get()方法中使用了读锁,get()方法可以被多个线程同时执行读操作;put()方法内部使用写锁,也就是说,put()方法在同一时刻只能有一个线程对缓存进行写操作。
这里需要注意的是:无论是读锁还是写锁,锁的释放操作都需要放到finally{}代码块中。
在以往的经验中,有两种向缓存中加载数据的方式,一种是:项目启动时,将数据全量加载到缓存中,一种是在项目运行期间,按需加载所需要的缓存数据。
接下来,我们就分别来看看全量加载缓存和按需加载缓存的方式。
全量加载缓存
全量加载缓存相对来说比较简单,就是在项目启动的时候,将数据一次性加载到缓存中,这种情况适用于缓存数据量不大,数据变动不频繁的场景,例如:可以缓存一些系统中的数据字典等信息。整个缓存加载的大体流程如下所示。
将数据全量加载到缓存后,后续就可以直接从缓存中读取相应的数据了。
全量加载缓存的代码实现比较简单,这里,我就直接使用如下代码进行演示。
public class ReadWriteLockCache{ private final Map m = new HashMap<>(); private final ReadWriteLock rwl = new ReentrantReadWriteLock(); // 读锁 private final Lock r = rwl.readLock(); // 写锁 private final Lock w = rwl.writeLock(); public ReadWriteLockCache(){ //查询数据库 List > list = .....; if(!CollectionUtils.isEmpty(list)){ list.parallelStream().forEach((f) ->{m.put(f.getK(), f.getV);}); } } // 读缓存 public V get(K key) { r.lock(); try { return m.get(key); } finally { r.unlock(); } } // 写缓存 public V put(K key, V value) { w.lock(); try { return m.put(key, value); } finally { w.unlock(); } }}
按需加载缓存
按需加载缓存也可以叫作懒加载,就是说:需要加载的时候才会将数据加载到缓存。具体来说:就是程序启动的时候,不会将数据加载到缓存,当运行时,需要查询某些数据,首先检测缓存中是否存在需要的数据,如果存在,则直接读取缓存中的数据,如果不存在,则到数据库中查询数据,并将数据写入缓存。后续的读取操作,因为缓存中已经存在了相应的数据,直接返回缓存的数据即可。
这种查询缓存的方式适用于大多数缓存数据的场景。
我们可以使用如下代码来表示按需查询缓存的业务。
class ReadWriteLockCache{ private final Map m = new HashMap<>(); private final ReadWriteLock rwl = new ReentrantReadWriteLock(); private final Lock r = rwl.readLock(); private final Lock w = rwl.writeLock(); V get(K key) { V v = null; //读缓存 r.lock(); try { v = m.get(key); } finally{ r.unlock(); } //缓存中存在,返回 if(v != null) { return v; } //缓存中不存在,查询数据库 w.lock(); try { //再次验证缓存中是否存在数据 v = m.get(key); if(v == null){ //查询数据库 v=从数据库中查询出来的数据 m.put(key, v); } } finally{ w.unlock(); } return v; }}
这里,在get()方法中,首先从缓存中读取数据,此时,我们对查询缓存的操作添加了读锁,查询返回后,进行解锁操作。判断缓存中返回的数据是否为空,不为空,则直接返回数据;如果为空,则获取写锁,之后再次从缓存中读取数据,如果缓存中不存在数据,则查询数据库,将结果数据写入缓存,释放写锁。最终返回结果数据。
这里,有小伙伴可能会问:为啥程序都已经添加写锁了,在写锁内部为啥还要查询一次缓存呢?
这是因为在高并发的场景下,可能会存在多个线程来竞争写锁的现象。例如:第一次执行get()方法时,缓存中的数据为空。如果此时有三个线程同时调用get()方法,同时运行到 w.lock()代码处,由于写锁的排他性。此时只有一个线程会获取到写锁,其他两个线程则阻塞在w.lock()处。获取到写锁的线程继续往下执行查询数据库,将数据写入缓存,之后释放写锁。
此时,另外两个线程竞争写锁,某个线程会获取到锁,继续往下执行,如果在w.lock()后没有v = m.get(key); 再次查询缓存的数据,则这个线程会直接查询数据库,将数据写入缓存后释放写锁。最后一个线程同样会按照这个流程执行。
这里,实际上第一个线程已经查询过数据库,并且将数据写入缓存了,其他两个线程就没必要再次查询数据库了,直接从缓存中查询出相应的数据即可。所以,在w.lock()后添加v = m.get(key); 再次查询缓存的数据,能够有效的减少高并发场景下重复查询数据库的问题,提升系统的性能。
读写锁的升降级
关于锁的升降级,小伙伴们需要注意的是:在ReadWriteLock中,锁是不支持升级的,因为读锁还未释放时,此时获取写锁,就会导致写锁永久等待,相应的线程也会被阻塞而无法唤醒。
虽然不支持锁升级,但是ReadWriteLock支持锁降级,例如,我们来看看官方的ReentrantReadWriteLock示例,如下所示。
class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { // Must release read lock before acquiring write lock rwl.readLock().unlock(); rwl.writeLock().lock(); try { // Recheck state because another thread might have // acquired write lock and changed state before we did. if (!cacheValid) { data = ... cacheValid = true; } // Downgrade by acquiring read lock before releasing write lock rwl.readLock().lock(); } finally { rwl.writeLock().unlock(); // Unlock write, still hold read } } try { use(data); } finally { rwl.readLock().unlock(); } }}}
数据同步问题
首先,这里说的数据同步指的是数据源和数据缓存之间的数据同步,说的再直接一点,就是数据库和缓存之间的数据同步。
这里,我们可以采取三种方案来解决数据同步的问题,如下图所示
超时机制
这个比较好理解,就是在向缓存写入数据的时候,给一个超时时间,当缓存超时后,缓存的数据会自动从缓存中移除,此时程序再次访问缓存时,由于缓存中不存在相应的数据,查询数据库得到数据后,再将数据写入缓存。
定时更新缓存
这种方案是超时机制的增强版,在向缓存中写入数据的时候,同样给一个超时时间。与超时机制不同的是,在程序后台单独启动一个线程,定时查询数据库中的数据,然后将数据写入缓存中,这样能够在一定程度上避免缓存的穿透问题。
点击关注,第一时间了解华为云新鲜技术~
关键词:
为你推荐
-
如何用ReadWriteLock实现一个通用的缓存中心? 微资讯
-
国内首创四价亚单位流感疫苗来了!疫苗行业诞生新兴黑马
-
2023天津郭静靓好时光巡演门票多少钱?
-
毕业证照片要求多大kb_毕业证照片要求-天天百事通
-
焦点热门:既算“经济账”又算“生态账” 湖南启动2023年自然资源资产离任(任中)审计
-
图灵波浪5.30-黄金冲高回落、跌势将持续|全球微动态
-
东南网架:5月29日融资买入241.92万元,融资融券余额1.12亿元-天天微头条
-
软科大学排名怎么样的可靠吗 全球报道
-
曾号称深大“第二食堂”的桂庙新村旧改全部完成签约 当前快看
-
今日观点!讨要11.6万加班费被公司指虚假加班 法院:给5.6万
-
隐私瘙痒用什么药膏_外阴又痒又疼用什么药
-
别让小麦提前发芽,否则你会失去更多……-世界新消息
-
环球热点!2b学校哪个比较好-2b学校排名
-
热文:环球热点评!阿里云智能CTO周靖人:MaaS已成行业标准,未来将围绕模型开发 天天百事通
-
菊花的茶的种类跟药效_菊花茶有什么种类 各有什么功用
-
全球信息:丰收带来喜悦 蓝莓产业已成为当地乡村振兴新引擎
-
四通一达快递加盟官网_四通一达快递加盟
-
墨尔本公寓大楼有自己的联合办公空间
-
4月企业资产证券化产品新增备案规模逾700亿元-全球实时
-
昆明市“四化协同”凝聚春城“新”力量-焦点信息
推荐内容
- 如何用ReadWriteLock实现一个通用的缓存中心? 微资讯
- 国内首创四价亚单位流感疫苗来了!疫苗行业诞生新
- 2023天津郭静靓好时光巡演门票多少钱?
- 毕业证照片要求多大kb_毕业证照片要求-天天百事通
- 焦点热门:既算“经济账”又算“生态账” 湖南启
- 图灵波浪5.30-黄金冲高回落、跌势将持续|全球微动态
- 东南网架:5月29日融资买入241.92万元,融资融券
- 软科大学排名怎么样的可靠吗 全球报道
- 曾号称深大“第二食堂”的桂庙新村旧改全部完成签
- 今日观点!讨要11.6万加班费被公司指虚假加班 法
- 隐私瘙痒用什么药膏_外阴又痒又疼用什么药
- 别让小麦提前发芽,否则你会失去更多……-世界新
- 环球热点!2b学校哪个比较好-2b学校排名
- 热文:环球热点评!阿里云智能CTO周靖人:MaaS已
- 菊花的茶的种类跟药效_菊花茶有什么种类 各有什
- 全球信息:丰收带来喜悦 蓝莓产业已成为当地乡村
- 四通一达快递加盟官网_四通一达快递加盟
- 墨尔本公寓大楼有自己的联合办公空间
- 4月企业资产证券化产品新增备案规模逾700亿元-全
- 昆明市“四化协同”凝聚春城“新”力量-焦点信息
- 天天关注:沉思者图片(沉思者)
- 天天速递!柯基犬把“自己”弄丢了,民警联动帮它
- 【环球新视野】10月龄宝宝鸡蛋羹的家常做法?
- 易动宇航完成超亿元 B 轮融资,鼎晖百孚、联想
- 今日热闻!德迈仕:公司电枢轴等多个量产产品已应
- 曲嘴森莺(关于曲嘴森莺介绍)
- 【视频】女子驾车时突然心慌、手抖,交警充当“代
- 叙利亚军方:大马士革周边地区遭以色列导弹袭击
- 俄总统普京签署废止《欧洲常规武装力量条约》法案
- 每日观点:金科股份盘中澄清与某大型国企集团合作
- 熬小米粥用热水还是凉水_熬小米粥
- 又一家!还是“世界500强”旗下房企,14万股民注
- 玩偶姐姐图片_姐姐的绣惑 每日看点
- 坐着公交去看海 青岛温馨巴士看海乘车攻略请收好
- 【微济阳】分房了!济阳这俩城中村近200户居民分
- 世界速讯:综合消息:“汉语桥”比赛在多国举行
- “旁观者量子比特”最大限度减少计算错误 环球即
- 天天速看:甲醇行情周报(5.22-5.26)
- 就业形势异常严峻!印度“经济奇迹”背后藏“定时
- 当前视讯!对虚假广告说不!惠州发布房地产中介行
油气
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
经济
-
中新网通辽10月18日电 (记者 张林虎)18日,记者从内蒙古自治区通辽市奈曼旗公安局获悉,国家一级保护动物--梅花鹿误入当地村民羊群,
-
中新网杭州10月18日电 (王题题 胡燕婕)云天收夏色,浅秋正渐浓。10月18日,浙江杭州市西湖游船有限公司推出的惠民多站点“西湖环湖游
-
中新网福州10月18日电 (记者 龙敏 王东明)福州市晋安区官方18日晚间通报,18日14时47分,晋安区岳峰镇化工路爱摩轮商业广场项目摩天
-
中新网兰州10月18日电 (闫姣 艾庆龙 吉翔)“红山白土头,黄河向西流。”不少人疑问,天下黄河向东流,为何甘肃永靖县这段黄河却向西
-
中新网北京10月18日电 《清华城市健康设施指数》18日在北京发布。报告成果显示,城市健康设施指数领先城市以中心城市和东部沿海城市