如何用Redis实现延迟队列
背景
前段时间有个小项目需要使用延迟任务,谈到延迟任务,我脑子第一时间一闪而过的就是使用消息队列来做,比如RabbitMQ的死信队列又或者RocketMQ的延迟队列,但是奈何这是一个小项目,并没有引入MQ,我也不太想因为一个延迟任务就引入MQ,增加系统复杂度,所以这个方案直接就被pass了。
虽然基于MQ这个方式走不通了,但是这个项目中使用到Redis,所以我就想是否能够使用Redis来代替MQ实现延迟队列的功能,于是我就查了一下有没有现成可用的方案,别说,还真给我查到了两种方案,并且我还仔细研究对比了这两个方案,发现要想很好的实现延迟队列,并不简单。
监听过期key
基于监听过期key的方式来实现延迟队列是我查到的第一个方案,为了弄懂这个方案实现的细节,我还特地去扒了扒官网,还真有所收获
1、Redis发布订阅模式
一谈到发布订阅模式,其实一想到的就是MQ,只不过Redis也实现了一套,并且跟MQ贼像,如图:
图中的channel的概念跟MQ中的topic的概念差不多,你可以把channel理解成MQ中的topic。
生产者在消息发送时需要到指定发送到哪个channel上,消费者订阅这个channel就能获取到消息。
2、keyspace notifications
在Redis中,有很多默认的channel,只不过向这些channel发送消息的生产者不是我们写的代码,而是Redis本身。当消费者监听这些channel时,就可以感知到Redis中数据的变化。
这个功能Redis官方称为keyspace notifications,字面意思就是键空间通知。
这些默认的channel被分为两类:
以__keyspace@<db>__:为前缀,后面跟的是key的名称,表示监听跟这个key有关的事件。
举个例子,现在有个消费者监听了__keyspace@0__:sanyou这个channel,sanyou就是Redis中的一个普通key,那么当sanyou这个key被删除或者发生了其它事件,那么消费者就会收到sanyou这个key删除或者其它事件的消息
以__keyevent@<db>__:为前缀,后面跟的是消息事件类型,表示监听某个事件
同样举个例子,现在有个消费者监听了__keyevent@0__:expired这个channel,代表了监听key的过期事件。那么当某个Redis的key过期了(expired),那么消费者就能收到这个key过期的消息。如果把expired换成del,那么监听的就是删除事件。具体支持哪些事件,可从官网查。
上述db是指具体的数据库,Redis不是默认分为16个库么,序号从0-15,所以db就是0到15的数字,示例中的0就是指0对应的数据库。
3、延迟队列实现原理
通过对上面的两个概念了解之后,应该就对监听过期key的实现原理一目了然了,其实就是当这个key过期之后,Redis会发布一个key过期的事件到__keyevent@<db>__:expired这个channel,只要我们的服务监听这个channel,那么就能知道过期的Key,从而就算实现了延迟队列功能。
所以这种方式实现延迟队列就只需要两步:
发送延迟任务,key是延迟消息本身,过期时间就是延迟时间
监听__keyevent@<db>__:expired这个channel,处理延迟任务
4、demo
好了,基本概念和核心原理都说完了之后,又到了show me the code环节。
好巧不巧,Spring已经实现了监听__keyevent@*__:expired这个channel这个功能,__keyevent@*__:expired中的*代表通配符的意思,监听所有的数据库。
所以demo写起来就很简单了,只需3步即可
引入pom
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>2.2.5.RELEASE</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.2.5.RELEASE</version></dependency>
配置类
@Configurationpublic class RedisConfiguration {@Beanpublic RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();redisMessageListenerContainer.setConnectionFactory(connectionFactory);return redisMessageListenerContainer;}@Beanpublic KeyExpirationEventMessageListener redisKeyExpirationListener(RedisMessageListenerContainer redisMessageListenerContainer) {return new KeyExpirationEventMessageListener(redisMessageListenerContainer);}}
KeyExpirationEventMessageListener实现了对__keyevent@*__:expiredchannel的监听
当KeyExpirationEventMessageListener收到Redis发布的过期Key的消息的时候,会发布RedisKeyExpiredEvent事件
所以我们只需要监听RedisKeyExpiredEvent事件就可以拿到过期消息的Key,也就是延迟消息。
对RedisKeyExpiredEvent事件的监听实现MyRedisKeyExpiredEventListener
@Component
public class MyRedisKeyExpiredEventListener implements ApplicationListener<RedisKeyExpiredEvent> {@Overridepublic void onApplicationEvent(RedisKeyExpiredEvent event) {byte[] body = event.getSource();System.out.println("获取到延迟消息:" + new String(body));}}
整个工程目录也简单
代码写好,启动应用
之后我直接通过Redis命令设置消息,就没通过代码发送消息了,消息的key为sanyou,值为task,值不重要,过期时间为5s
set sanyou task
expire sanyou 5
如果上面都理论都正确,不出意外的话,5s后MyRedisKeyExpiredEventListener应该可以监听到sanyou这个key过期的消息,也就相当于拿到了延迟任务,控制台会打印出获取到延迟消息:sanyou。
于是我满怀希望,静静地等待了5s。。
5、4、3、2、1,时间一到,我查看控制台,但是控制台并没有按照预期打印出上面那句话。
为什么会没打印出?难道是代码写错了?正当我准备检查代码的时候,官网的一段话道出了真实原因。
我给大家翻译一下上面这段话讲的内容。
上面这段话主要讨论的是key过期事件的时效性问题,首先提到了Redis过期key的两种清除策略,就是面试八股文常背的两种:
惰性清除。当这个key过期之后,访问时,这个Key才会被清除
定时清除。后台会定期检查一部分key,如果有key过期了,就会被清除
再后面那段话是核心,意思是说,key的过期事件发布时机并不是当这个key的过期时间到了之后就发布,而是这个key在Redis中被清理之后,也就是真正被删除之后才会发布。
到这我终于明白了,上面的例子中即使我设置了5s的过期时间,但是当5s过去之后,只要两种清除策略都不满足,没人访问sanyou这个key,后台的定时清理的任务也没扫描到sanyou这个key,那么就不会发布key过期的事件,自然而然也就监听不到了。
至于后台的定时清理的任务什么时候能扫到,这个没有固定时间,可能一到过期时间就被扫到,也可能等一定时间才会被扫到,这就可能会造成了客户端从发布到监听到的消息时间差会大于等于过期时间,从而造成一定时间消息的延迟,这就着实有点坑了。。
5、坑
除了上面测试demo的时候遇到的坑之外,在我深入研究之后,还发现了一些更离谱的坑。
丢消息太频繁
Redis的丢消息跟MQ不一样,因为MQ都会有消息的持久化机制,可能只有当机器宕机了,才会丢点消息,但是Redis丢消息就很离谱,比如说你的服务在重启的时候就消息会丢消息。
Redis实现的发布订阅模式,消息是没有持久化机制,当消息发布到某个channel之后,如果没有客户端订阅这个channel,那么这个消息就丢了,并不会像MQ一样进行持久化,等有消费者订阅的时候再给消费者消费。
所以说,假设服务重启期间,某个生产者或者是Redis本身发布了一条消息到某个channel,由于服务重启,没有监听这个channel,那么这个消息自然就丢了。
消息消费只有广播模式
Redis的发布订阅模式消息消费只有广播模式一种。
所谓的广播模式就是多个消费者订阅同一个channel,那么每个消费者都能消费到发布到这个channel的所有消息。
如图,生产者发布了一条消息,内容为sanyou,那么两个消费者都可以同时收到sanyou这条消息。
所以,如果通过监听channel来获取延迟任务,那么一旦服务实例有多个的话,还得保证消息不能重复处理,额外地增加了代码开发量。
接收到所有key的某个事件
这个不属于Redis发布订阅模式的问题,而是Redis本身事件通知的问题。
当消费者监听了以__keyevent@<db>__:开头的消息,那么会导致所有的key发生了事件都会被通知给消费者。
举个例子,某个消费者监听了__keyevent@*__:expired这个channel,那么只要key过期了,不管这个key是张三还会李四,消费者都能收到。
所以如果你只想消费某一类消息的key,那么还得自行加一些标记,比如消息的key加个前缀,消费的时候判断一下带前缀的key就是需要消费的任务。
所以,综上能够得出一个非常重要的结论,那就是监听Redis过期Key这种方式实现延迟队列,不稳定,坑贼多!
那有没有比较靠谱的延迟队列的实现方案呢?这就不得不提到我研究的第二种方案了。
Redisson实现延迟队列
Redisson他是Redis的儿子(Redis son),基于Redis实现了非常多的功能,其中最常使用的就是Redis分布式锁的实现,但是除了实现Redis分布式锁之外,它还实现了延迟队列的功能。
先来个demo,后面再来说说这种实现的原理。
1、demo
引入pom
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.13.1</version></dependency>
封装了一个RedissonDelayQueue类
@Component@Slf4jpublic class RedissonDelayQueue {private RedissonClient redissonClient;private RDelayedQueue<String> delayQueue;private RBlockingQueue<String> blockingQueue;@PostConstructpublic void init() {initDelayQueue();startDelayQueueConsumer();}private void initDelayQueue() {Config config = new Config();SingleServerConfig serverConfig = config.useSingleServer();serverConfig.setAddress("redis://localhost:6379");redissonClient = Redisson.create(config);blockingQueue = redissonClient.getBlockingQueue("SANYOU");delayQueue = redissonClient.getDelayedQueue(blockingQueue);}private void startDelayQueueConsumer() {new Thread(() -> {while (true) {try {String task = blockingQueue.take();log.info("接收到延迟任务:{}", task);} catch (Exception e) {e.printStackTrace();}}}, "SANYOU-Consumer").start();}public void offerTask(String task, long seconds) {log.info("添加延迟任务:{} 延迟时间:{}s", task, seconds);delayQueue.offer(task, seconds, TimeUnit.SECONDS);}}
这个类在创建的时候会去初始化延迟队列,创建一个RedissonClient对象,之后通过RedissonClient对象获取到RDelayedQueue和RBlockingQueue对象,传入的队列名字叫SANYOU,这个名字无所谓。
当延迟队列创建之后,会开启一个延迟任务的消费线程,这个线程会一直从RBlockingQueue中通过take方法阻塞获取延迟任务。
添加任务的时候是通过RDelayedQueue的offer方法添加的。
controller类,通过接口添加任务,延迟时间为5s
@RestController
public class RedissonDelayQueueController {@Resourceprivate RedissonDelayQueue redissonDelayQueue;@GetMapping("/add")public void addTask(@RequestParam("task") String task) {redissonDelayQueue.offerTask(task, 5);}}
启动项目,在浏览器输入如下连接,添加任务
http://localhost:8080/add?task=sanyou
静静等待5s,成功获取到任务。
2、实现原理
如下图就是上面demo中,一个延迟队列会在Redis内部使用到的channel和数据类型
SANYOU前面的前缀都是固定的,Redisson创建的时候会拼上前缀。
redisson_delay_queue_timeout:SANYOU,sorted set数据类型,存放所有延迟任务,按照延迟任务的到期时间戳(提交任务时的时间戳 + 延迟时间)来排序的,所以列表的最前面的第一个元素就是整个延迟队列中最早要被执行的任务,这个概念很重要
redisson_delay_queue:SANYOU,list数据类型,也是存放所有的任务,但是研究下来发现好像没什么用。。
SANYOU,list数据类型,被称为目标队列,这个里面存放的任务都是已经到了延迟时间的,可以被消费者获取的任务,所以上面demo中的RBlockingQueue的take方法是从这个目标队列中获取到任务的
redisson_delay_queue_channel:SANYOU,是一个channel,用来通知客户端开启一个延迟任务
有了这些概念之后,再来看看整体的运行原理图
生产者在提交任务的时候将任务放到redisson_delay_queue_timeout:SANYOU中,分数就是提交任务的时间戳+延迟时间,就是延迟任务的到期时间戳
客户端会有一个延迟任务,为了区分,后面我都说是客户端延迟任务。这个延迟任务会向Redis Server发送一段lua脚本,Redis执行lua脚本中的命令,并且是原子性的
这段lua脚本主要干了两件事:
将到了延迟时间的任务从redisson_delay_queue_timeout:SANYOU中移除,存到SANYOU这个目标队列
获取到redisson_delay_queue_timeout:SANYOU中目前最早到过期时间的延迟任务的到期时间戳,然后发布到redisson_delay_queue_channel:SANYOU这个channel中
当客户端监听到redisson_delay_queue_channel:SANYOU这个channel的消息时,会再次提交一个客户端延迟任务,延迟时间就是消息(最早到过期时间的延迟任务的到期时间戳)- 当前时间戳,这个时间其实也就是redisson_delay_queue_channel:SANYOU中最早到过期时间的任务还剩余的延迟时间。
此处可以等待10s,好好想想。。
这样,一旦时间来到了上面说的最早到过期时间任务的到期时间戳,redisson_delay_queue_timeout:SANYOU中上面说的最早到过期时间的任务已经到期了,客户端的延迟任务也同时到期,于是开始执行lua脚本操作,及时将到了延迟时间的任务放到目标队列中。然后再次发布剩余的延迟任务中最早到期的任务到期时间戳到channe中,如此循环往复,一直运行下去,保证redisson_delay_queue_timeout:SANYOU中到期的数据能及时放到目标队列中。
所以,上述说了一大堆的主要的作用就是保证到了延迟时间的任务能够及时被放到目标队列。
这里再补充两个特殊情况,图中没有画出:
第一个就是如果redisson_delay_queue_timeout:SANYOU是新添加的任务(队列之前有或者没有任务)是队列中最早需要被执行的,也会发布消息到channel,之后就按时上面说的流程走了。
添加任务代码如下,也是通过lua脚本来的
第二种特殊情况就是项目启动的时候会执行一次客户端延迟任务。项目在重启时,由于没有客户端延迟任务的执行,可能会出现redisson_delay_queue_timeout:SANYOU队列中有到期但是没有被放到目标队列的可能,重启就执行一次就是为了保证到期的数据能被及时放到目标队列中。
3、与第一种方案比较
现在来比较一下第一种方案和Redisson的这种方案,看看有没有第一种方案的那些坑。
第一个任务延迟的问题,Redisson方案理论上是没有延迟的,但是当消息数量增加,消费者消费缓慢这个情况下可能会导致延迟任务消费的延迟。
第二个丢消息的问题,Redisson方案很大程度上减轻了丢消息的可能性,因为所有的任务都是存在list和sorted set两种数据类型中,Redis有持久化机制,就算Redis宕机了,也就可能会丢一点点数据。
第三个广播消费任务的问题,这个是不会出现的,因为每个客户端都是从同一个目标队列中获取任务的。
第四个问题是Redis内部channel发布事件的问题,跟这种方案不沾边,就更不可能存在了。
所以,通过上面的对比可以看出,Redisson这种实现方案就显得更加的靠谱了。
相关文章:
如何用Redis实现延迟队列
背景前段时间有个小项目需要使用延迟任务,谈到延迟任务,我脑子第一时间一闪而过的就是使用消息队列来做,比如RabbitMQ的死信队列又或者RocketMQ的延迟队列,但是奈何这是一个小项目,并没有引入MQ,我也不太想…...
项目文件相关总结
风险登记册 风险登记册记录了已识别单个风险的详细信息。其主要内容包括: 已识别的风险清单潜在的风险责任人潜在的风险应对措施清单风险管理计划要求的其他信息供方选择标准 供方选择标准用于确保选出的建议书将提供最佳质量的所需服务,主要内容 包括: 能力和潜力产品成本…...
ZooKeeper集群搭建步骤
一、准备虚拟机准备三台虚拟机,对应ip地址和主机名如下:ip地址Hostname192.168.153.150ant163192.168.153.151ant164192.168.153.152ant165修改hostname,并使之生效[rootlocalhost /]# hostnamectl set-hostname zookeeper1 //修改hostname …...
网际协议IP
网际协议IP 文章目录网际协议IP[toc]虚拟互联网IP地址及其表示方法分类IP地址(两级)无分类编址 CIDR网路前缀地址块地址掩码子网划分(三级IP地址)IP地址和MAC地址地址解析协议ARPIP数据报的格式IP数据报首部的固定部分中的各字段IP数据报首部的可变部分分…...
Python 语言参考手册、教程、标准库
官方文档:https://docs.python.org/zh-cn/3.11/ Python 语言参考手册 介绍了 Python 句法与“核心语义”。在力求简明扼要的同时,我们也尽量做到准确、完整。有关内置对象类型、内置函数、模块的语义在 Python 标准库 中介绍。有关本语言的非正式介绍&am…...
数据库连接池 BoneCP、HikariCP 等
文章目录 数据库连接池 BoneCP、HikariCP 等BoneCPDruidTomcat Jdbc PoolHikariCPC3p0DbcpLRUPSCachePS数据库连接池 BoneCP、HikariCP 等 BoneCP 官方说法 BoneCP 是一个高效、免费、开源的 Java 数据库连接池实现库 设计初衷就是为了提高数据库连接池性能,根据某些测试数…...
博客系统 SSM 超强硬核良心推荐之第一弹 - 预备工作
硬核 ! 从 0 到 1 完美实现 SSM 版本的博客系统 , 学会保准不吃亏!一 . SSM 版本相比于 Servlet 版本的亮点二 . 初始化数据库三 . 前端页面3.1 注册页面3.2 登录功能3.3 文章总列表页3.4 自己的文章列表页3.5 文章详情页3.6 编写博客页面大家好 , 这是新的专栏 , 博客系统 SSM…...
[Web] 简单瀑布流布局实现
目前的纯 CSS 布局, 是没办法实现比较完美的瀑布流布局的. 参考: CSS总结:瀑布流布局 - 黑白程序员 我使用 JS CSS, 并且自动布局实现了较为简单, 观赏性好的瀑布流布局. 代码 HTML: <!DOCTYPE html> <html lang"en"> <head><link rel&quo…...
多线程之死锁,哲学家就餐问题的实现
1.死锁是什么 死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。 2.哲学家就餐问题 有五个哲学家,他们的生活方式是交替地进行思考和进餐…...
UTF-8编码
介绍 UTF-8 编码 UTF-8 是一种针对 Unicode 的可变长度字符编码。 针对 Unicode:UTF-8 是 Unicode 的实现方式之一。相当于 Unicode 规定了字符对应的代码值,这个代码值需要转换为字节序列的形式,用于数据存储、传输。代码值到字节序列的转…...
likeshop单商户SaaS版V1.8.2说明!
likeshop单商户SaaS版V1.8.2主要更新如下: 新增 前端登录引导用户填写头像昵称 PC端—注册页面显示服务协议和隐私政策 PC端—首次进入商城弹出协议提示 PC端—结算页新增门店自提的配送方式 后台—PC端菜单导航栏的跳转链接支持添加自定义链接 优…...
算法训练营 day46 动态规划 最后一块石头的重量 II 目标和 一和零
算法训练营 day46 动态规划 最后一块石头的重量 II 目标和 一和零 最后一块石头的重量 II 1049. 最后一块石头的重量 II - 力扣(LeetCode) 有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。 每一回合…...
nginx-host绕过实例复现
绕过Nginx Host限制第一种处理方法Nginx在处理Host的时候,会将Host用冒号分割成hostname和port,port部分被丢弃。所以,我们可以设置Host的值为2023.mhz.pw:xxx"example.com,这样就能访问到目标Server块:第二种处理…...
Java学习记录day9
类与对象 内部类 成员内部类 在一个类的内部定义的类。 public class Outer {private int a 10;public void outMethod() {System.out.println("这是外部类中的方法");}// 成员内部类public class Inner{private int b 10;public void innerMethod() {// 外部类…...
ActiveReports.NET 17.0 Crack by Xacker
一个完整的报告解决方案,用于在您的业务应用程序中设计、定制、发布和查看报告。 ActiveReports.NET 通过直观的 Visual Studio 集成报表设计器和丰富的控件帮助您提供精美的报表。ActiveReports 提供基于代码的跨平台报告、易于使用的设计器和灵活的 API。适用于桌…...
【计算机网络】传输层TCP协议
文章目录认识TCP协议TCP协议的格式字段的含义序号与确认号六个标志位窗口大小确认应答(ACK)机制超时重传机制连接管理机制三次握手四次挥手滑动窗口流量控制拥塞控制延迟应答捎带应答面向字节流粘包问题TCP异常情况总结认识TCP协议 传输控制协议 (TCP,T…...
Mysql5.7安装【Windows版】
文章目录一、下载二、添加到环境变量三、添加配置文件my.ini四、安装Mysql 修改密码一、下载 下载地址 滑倒最下面有一个MySQL Community Server 选择要下载的版本 二、添加到环境变量 下载好了之后开始解压 把bin目录添加到环境变量 可以点击进入bin目录,直接复…...
分布式一致性算法Raft原理图释
什么是分布式一致性算法Raft 分布式一致性算法Raft:指在分布式场景下实现集群数据同步的解决方案 掌握了这个算法,就可以较容易地处理绝大部分场景的容错和数据一致性需求 Raft三大角色 跟随者(Follower):普通群众…...
网络安全-字典生成-crunch
网络安全-字典生成-crunch crunch工具,在kali已经集成好了 2是代表最小字符长度 4是最大字符长度 生成了一个2M的文件 还有我们来查看这个密码本 从abcd26个英文字母的2位到4位的组合,他全部排列了一次 还可以自定义数字,特殊字符…...
闪光桐人の实习日记
2023年2月13日 1,认识了职场礼仪,学习了职场礼仪的重要性 尊重->心情愉悦->建立信任与好感->合作机遇的敲门砖 2,学习了职场礼仪中的邮件礼仪 模板管理中设置自己的名片 部门写到三级部,如果部门名太长要换一行 发送…...
PostgreSQL 常见配置参数
max_wal_size : 两个检查点(checkpoint)之间,WAL可增长的最大大小,即:自动WAL checkpoint允许WAL增长的最大值。该值缺省是1GB。如果提高该参数值会提升性能,但也是会消耗更多空间、同时会延长崩溃恢复所需…...
JAVA 常用类型之String结构
String在java中我们是用来操作字符串的,但它的底层结构确是一个char[]数组,通过数组的方式将每个字符进行保存。 使用时:String str"ABCD",内部存value确是:value[A,B,C,D]; 如下图: 参考String源…...
二三层网络设备封装与解封装原理
1、寻址转发(寻址指的是寻找IP地址) 路由表放在一个公共的地方,比如主控板上,由主控板 的CPU运行路由协议,计算路由,生成和维护路由表。 转发表与路由表: 转发表是根据路由表生成的。路由表中…...
9、MyBatis框架——使用注解开发实现数据库增删改查操作、一级缓存、二级缓存、MyBatis实现分页
目录 一、使用注解开发实现数据库增删改查操作 1、搭建项目 2、使用注解开发操作数据库 二、一级缓存 1、一级缓存失效的情况 三、二级缓存 1、手动开启二级缓存cacheEnabled 2、二级缓存机制 四、MyBatis实现分页 1、配置环境 2、startPage()开启分页 3、PageInfo…...
C++STL剖析(六)—— set和multiset的概念和使用
文章目录🌟 前言🍑 树型结构和哈希结构🍑 键值对1. set的介绍和使用🍑 set的模板参数列表🍑 set的构造🍑 set的使用🍅 insert🍅 find🍅 erase🍅 swap…...
SpringColud第四讲 Nacos的Windows安装方式和Linux的安装方式
在Nacos的GitHub页面,提供有下载链接,可以下载编译好的Nacos服务端或者源代码: 目录 1.Windows安装Nacos 1.1.下载 1.2.解压 1.3.修改相关配置: 1.4.启动: 1.5.登录: 2.Linux的安装方式Nacos 2.1.…...
微服务项目【网关服务限流熔断降级分布式事务】
网关服务限流熔断降级 第1步:启动sentinel-dashboard控制台和Nacos注册中心服务 第2步:在网关服务中引入sentinel依赖 <!-- sentinel --> <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-…...
【情人节用Compose给女神写个爱心动画APP】
情人节用Compose给女神写个爱心动画APP前言涉及知识点实现思路实现过程绘制爱心创建动画效果Preview预览效果完整源码彩蛋前言 前一阵子看电视里的学霸用代码写了个炫酷的爱心,网上有很多js和python的源码,复制粘贴就能拥有,但是Android的好…...
GUI swing和awt
GUI(Graphical User Interface,简称 GUI,图形用户界面)是指采用图形方式显示的计算机操作用户界面,与早期计算机使用的命令行界面相比,图形界面对于用户来说在视觉上更易于接受。Java GUI主要有两个核心库&…...
速通Spring
尚硅谷2023最新版Spring6课程_bilibili 1 Spring 【强制】Spring是什么? 1) Spring是一款主流的Java EE轻量级开源框架。 轻量级:体积很小,且不需要依赖于其他组件。 2) 狭义的Spring。 Spring Framework。 3) 广义的Spring。 以Spring F…...
宜宾市政府网站建设/津seo快速排名
随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666) 转自:http://blog.csdn.net/eager3466/article/details/42931017 Eclipse中安卓开发遇到parseSdkContent failed Could not initialize class Andro…...
企业每月报账在哪个网站做/优化设计英语
Fisher Vector(FV)是一种类似于BOVW词袋模型的一种编码方式,如提取图像的SIFT特征,通过矢量量化(KMeans聚类),构建视觉词典(码本),FV采用混合高斯模型&#x…...
南昌电商网站设计/提高工作效率心得体会
您可以使用StringDecoder从HttpRequest中的“List of Int”转换为“String”.因为无论你发送json,纯文本还是png,Dart总是以数据的形式发送数据“服务器列表”到服务器.另一种方法是使用在Heroku Steam上测试的Streams(http://www.dartlang.org/articles/feet-wet-streams/)v0.…...
wordpress代码安装畅言/福建网络seo关键词优化教程
见字如面,我是军哥!这几天一位读者私信我,说最近真太惨了,同时遇到小组项目被放弃,然后年底申请加薪被领导拒绝,租的房子是二房东遭遇大房东收房,反正是各种不顺。我开导他,每个人的…...
wordpress 动态解析/怎么注册域名网址
编辑:陈凌煜本文出自微信公众号“August精彩编程”(ID:august-edu)2019年8月12日网站分静态网站和动态网站,相信小伙伴们对这两个词略有耳闻或者已经了解,那么小编还是啰嗦一下这两种网站有什么区别。网页上的内容是随着数据库读取…...
安徽省住建厅网站官网/郑州疫情最新消息
首先 double mean[4]]{0.}; const double *& haha mean;//error 这种情况是非法的.原因是,这里的const限定的是double,也就是这是一个 “指向const double 的指针变量的引用“,所以,即使这个指针可以被改变,但是指…...