Java进阶架构实战——Redis在京东到家的订单中的使用
背景
Redis作为一款性能优异的内存数据库,在互联网公司有着多种应用场景,下面介绍下Redis在京东到家的订单列表中的使用场景。主要从以下几个方面来介绍:
- 订单列表在Redis中的存储结构
- Redis和DB数据一致性保证
- Redis中的分布式锁
- 缓存防穿透和雪崩
订单列表在Redis中的存储结构
- 订单列表数据在缓存中,是以用户的唯一标识作为键,以一个按下单时间倒序的有序集合为值进行存储的。大家都知道Redis的sorted set中每个元素都有一个分数,Redis就是根据这个分数排序的。订单有序集合中的每个元素是将时间毫秒数+订单号最后3位作为分数进行排序的。为什么不只用毫秒数作为分数呢?因为我们的下单时间只精确到秒,如果不加订单号最后3位,若同一秒有两个或两个以上订单时,排序分数就会一样,从而导致根据分数从缓存查询订单时不能保证唯一性。而我们的订单号的生成规则可以保证同一秒内的订单号的最后3位肯定不一样,从而可以解决上述问题。
- 有必要将一个用户的所有订单都放入缓存吗?针对用户订单是没有必要的,因为很少有用户去看很久以前的历史订单。真正的热点数据其实也就是最近下过的一些订单,所以,为了节省内存空间,只需要存放一个用户最近下过的N条订单就行了,这个N,相当于一个阀值,超过了这个阀值,再从数据库中查询订单数据,当然,这部分查库操作已经是很小概率的操作了。
Redis和DB数据一致性保证
只要有多份数据,就会涉及到数据一致性的问题。Redis和数据库的数据一致性,也是必然要面对的问题。我们这边的订单数据是先更新数据库,数据库更新成功后,再更新缓存,若数据库操作成功,缓存操作失败了,就出现了数据不一致的情况。保证数据一致性我们前后使用过两种方式:
- 方式一:
- 循环5次更新缓存操作,直到更新成功退出循环,这一步主要能减小由于网络瞬间抖动导致的更新缓存失败的概率,对于缓存接口长时间不可用,靠循环调用更新接口是不能补救接口调用失败的。
- 如果循环5次还没有更新成功,就通过worker去定时扫描出数据库的数据,去和缓存中的数据进行比较,对缓存中的状态不正确的数据进行纠正。
- 方式二:
- 跟方式一的第一步操作一样
- 若循环更新5次仍不成功,则发一个缓存更新失败的mq,通过消费mq去更新缓存,会比通过定时任务扫描更及时,也不会有扫库的耗时操作。此方式也是我们现在使用的方式。
代码示例:
Redis中的分布式锁
分布式锁常用的实现方式有Redis和zookeeper,本文主要介绍下Redis的分布式锁,然后再介绍下我们使用分布式锁的场景。
Redis分布式锁在2.6.12版本之后的实现方式比较简单,只需要使用一个命令即可:
SET key value [EX seconds] [NX]
其中,可选参数EX seconds :设置键的过期时间为 seconds 秒;NX :只在键不存在时,才对键进行设置操作。
这个命令相当于2.6.12之前的setNx和expire两个命令的原子操作命令。Redis的JAVA客户端分布式锁实现示例代码:
2.6.12版本之后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aqWc4llp-1654688901218)(https://upload-images.jianshu.io/upload_images/27964194-2125cffda8606e4e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
2.6.12版本之前,由于没有一个上述的原子命令,需要一些命令组合实现,但不能简单的使用setNx、expire这两个命令,因为如果setNx成功,expire命令失败时,恰好执行删除lockKey的也执行失败,key就永远不会过期,就会出现死锁问题,如:
第(1)步设置lockKey失效时间失败,lockKey在缓存永久保存。在此我向大家推荐一个架构学习交流圈。交流学习指导伪鑫:1253431195(里面有大量的面试题及答案)里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化、分布式架构等这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多
第(2)步没来得及释放锁时,系统崩溃,finally块没来得及执行,最终导致锁永远在缓存中,所有其他线程再也获取不到锁。所以不能单纯的依靠设置锁的失效时间来防止释放锁失败,需要通过下列方法防止这种情况,但比较繁琐,不过2.6.12版本之前也必须通过如下方法才更为妥当:
public booelan getLock(String lockKey) { boolean lock = false; while (!lock) { String expireTime = String.valueOf(System.currentTimeMillis() + 5000); // (1)第一个获得锁的线程,将lockKey的值设置为当前时间+5000毫秒,后面会判断,如果5秒之后,获得锁的线程还没有执行完,会忽略之前获得锁的线程,而直接获取锁,所以这个时间需要根据自己业务的执行时间来设置长短。 lock = shardedXCommands.setNX(lockKey, expireTime); if (lock) { // 已经获取了这个锁 直接返回已经获得锁的标识 return lock; } // 没获得锁的线程可以执行到这里:从Redis获取老的时间戳 String oldTimeStr = shardedXCommands.get(lockKey); if (oldTimeStr != null && !"".equals(oldTimeStr.trim())) { Long oldTimeLong = Long.valueOf(oldTimeStr); // 当前的时间戳 Long currentTimeLong = System.currentTimeMillis(); // (2)如果oldTimeLong小于当前时间了,说明之前持有锁的线程执行时间大于5秒了,就强制忽略该线程所持有的锁,重新设置自己的锁 if (oldTimeLong < currentTimeLong) { // (3)调用getset方法获取之前的时间戳,注意这里会出现多个线程竞争,但肯定只会有一个线程会拿到第一次获取到锁时设置的expireTime String oldTimeStr2 = shardedXCommands.getSet(lockKey, String.valueOf(System.currentTimeMillis() + 5000)); // (4)如果刚获取的时间戳和之前获取的时间戳一样的话,说明没有其他线程在占用这个锁,则此线程可以获取这个锁. if (oldTimeStr2 != null && oldTimeStr.equals(oldTimeStr2)) { lock = true; // 获取锁标记 break; } } } // 暂停50ms,重新循环 try { Thread.sleep(50); } catch (InterruptedException e) { log.error(e); } } return lock;}
上述方法主要使用了Redis的setNX、getSet两个方法,不依赖Redis的expire方法,即便是删除锁失败时,上面逻辑第(2)步也会规避这个问题。
- 订单使用分布式锁的场景是订单状态有变更的时候,需要先使用锁–>读缓存数据–>判断当前订单状态是否允许变更为别的状态–>更新缓存中的订单状态–>释放锁。
缓存防穿透和雪崩
- 缓存为我们挡住了80-90%甚至更多的流量,然而当缓存中的大量热点数据恰巧在差不多的时间过期时,或者当有人恶意伪造一些缓存中根本没有的数据疯狂刷接口时,就会有大量的请求直接穿透缓存访问到数据库(因为查询数据策略是缓存没有命中,就查数据库),给数据库造成巨大压力,甚至使数据库崩溃,这肯定是我们系统不允许出现的情况。我们需要针对这种情况进行处理。下图是处理流程图:
代码示例:
防止穿透和雪崩的关键地方在于使用分布式锁和锁的粒度控制。首先初始化了128(0-127)个锁,然后让所有缓存没命中的用户去竞争这128个锁,得到锁后并且再一次判断缓存中依然没有数据的,才有权利去查询数据库。没有将锁粒度限制到用户级别,是因为如果粒度太小的话,某一个时间点有太多的用户去请求,同样会有很多的请求打到数据库。比如:
在时间点T1有10000个用户的缓存数据失效了,恰恰他们又在时间点T1都请求数据,如果锁粒度是用户级别,那么这10000个用户都会有各自的锁,也就意味着他们都可以去访问数据库,同样会对数据库造成巨大压力。而如果是通过用户id去hashcode和127取模,意味着最多会产生128个锁,最多会有128个并发请求访问到数据库,其他的请求会由于没有竞争到锁而阻塞,待第一批获取到锁的线程释放锁之后,剩下的请求再进行竞争锁,但此次竞争到锁的线程,在执行代码段2中第4步时:orderRedisCache.isOrderListExist(userId),缓存中有可能已经有数据了,就不用再查数据库了,依次类推,从而可以挡住很多数据库请求,起到很好的保护数据库的作用。
总结
- 缓存中存放了用户的部分订单,且是以下单时间+订单号最后三位算出分数(这样做是为因为下单时间只精确到秒,为了防止同一秒下多个订单导致排序分数相同),进行排序的有序集合。
- 数据库更新成功,缓存更新失败,这样导致数据不一致,可以通过更新缓存失败后发mq的策略进行缓存更新尝试,比定时任务更高效,更及时。
- Redis分布式锁实现,2.6版本前,通过setNx和getSet两个命令实现,2.6版本之后,Redis提供了SET key value [EX seconds] [NX]这个命令可以实现。
- 防穿透和雪崩依赖了分布式锁,值得注意的是锁粒度不能细到用户级别,可以根据数据库性能和业务要求,算出合适的锁的数量,让所有未命中缓存的用户通过hashCode和锁数量取模,去竞争锁,得到锁的才获得查库权利。
最后,欢迎大家关注我的主页,希望分享的干货大家能喜欢
Java进阶架构实战——Redis在京东到家的订单中的使用相关推荐
- Redis在京东到家的订单中的使用
背景 Redis作为一款性能优异的内存数据库,在互联网公司有着多种应用场景,下面介绍下Redis在京东到家的订单列表中的使用场景.主要从以下几个方面来介绍: 订单列表在Redis中的存储结构 Redi ...
- 解密“达达-京东到家”的订单即时派发技术原理和实践
本文由达达京东到家Java工程师季炳坤原创分享. 1.前言 达达-京东到家作为优秀的即时配送物流平台,实现了多渠道的订单配送,包括外卖平台的餐饮订单.新零售的生鲜订单.知名商户的优质订单等.为了提升平 ...
- 2018.7-2019.7一周年Java进阶架构师技术文章整理 建议收藏
其实师长的公众号从2017年就开始发技术文章了,但是因为某些原因(就是懒)太监了许久,直到2018.7的时候才恢复更新.不知不觉中,已经更新了一年的广告,在没广告的日子里,顺带更新技术文章,截至201 ...
- java面向对象思维导图_2020年Java进阶架构师的必备思维导图,让你少走弯路!
架构师是什么?要做什么? 架构师 :是一个既需要掌控整体又需要洞悉局部瓶颈并依据具体的业务场景给出解决方案的团队领导型人物.架构师不是一个人,他需要建立高效的体系,带领团队去攻城略地,在规定的时间内完 ...
- Java秒杀系统实战系列~分布式唯一ID生成订单编号
摘要: 本篇博文是"Java秒杀系统实战系列文章"的第七篇,在本博文中我们将重点介绍 "在高并发,如秒杀的业务场景下如何生成全局唯一.趋势递增的订单编号",我们 ...
- 【JAVA进阶架构师指南】 小白勿点
前言 本博客是长篇系列博客,旨在帮助想提升自己,突破技术瓶颈,但又苦于不知道如何进行系统学习从而提升自己的童鞋.笔者假设读者具有3-5年开发经验,java基础扎实,想突破自己的技术瓶颈,成为一位优秀的 ...
- 13W 字!银四巨作:Java 进阶架构师核心手册
4 种线程池 线程生命周期(状态) 终止线程 4 种方式 sleep 与 wait 区别 start 与 run 区别 JAVA 后台线程 JAVA 锁 线程基本方法 4.1.11. 线程上下文切换 ...
- 【架构实战day1】京东开放平台的架构与演进
本文转自[程序架道 ID:xindongbook17]的精彩分享. 本篇文章一共分为四个部分,分别是开放生态.开放网关.开放授权和开放安全.为什么要做开放,开放的技术实现有哪些,主要是开放网关和授权, ...
- 【Java进阶】Elasticsearch应用之京东搜索
目录 京东搜索Elasticsearch 开发环境 项目概况 pom.xml 实现代码 ElasticSearchConfig 实体类 Content 工具类 HtmlParseUtil 业务逻辑层 ...
- Java进阶架构之路:如何从小白成为年薪百万的架构师
对于工作多年的程序员而言,日后的职业发展无非是继续专精技术.转型管理和晋升架构师三种选择. 架构师在一家公司有多重要.优秀架构师需要具备怎样的素质以及架构师的发展现状三个方面来分析 程序员如何才能晋升 ...
最新文章
- matlab画平行坐标轴的直线
- 关于Map迭代循环,key和value的顺序问题
- 1.二叉树的中序遍历
- 使用 jQuery 简化 Ajax 开发.
- 安卓相机 高帧率_Android MediaCodec和摄像头:如何实现更高的帧速率从相机获取帧原始数据?...
- 如何抓获JVM crash的幕后黑手
- robot framework集成Jenkins环境
- Hadoop集群环境下网络架构的设计与优化
- python根据频率画出词云_利用pandas+python制作100G亚马逊用户评论数据词云
- str_replace中的匹配空白符,必须用双引号
- STC学习:光敏计数
- UNITY2021 开发安卓app 扫描一维二维条码
- Deepin安装搜狗中文输入法
- seata启动报错的一种处理方式 NO channal is available for resource [deduct]
- 【风马一族_xml】xml语法
- Hark语音识别学习(一)-Hark desinger的使用
- VC2008下使用OpenSSL 1.0.0g(免编译)
- java怎么语音转换成文字_Annyang将语音转换为文本
- 非正式协议与正式协议的区别
- 魔兽世界怀旧服服务器信息,魔兽世界怀旧服服务器类型有哪些_怀旧服服务器类型介绍...