热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Go实战:抽奖系统核心技术点实现

Go实战:抽奖系统核心技术点实现考验系统并发能力最常见的无非是秒杀、抽奖等业务场景。我们不妨以一个支持高并发的抽奖系统的设计和实现为例来进行Go语言的实战学习。要

Go 实战 :抽奖系统核心技术点实现

考验系统并发能力最常见的无非是秒杀、抽奖等业务场景。我们不妨以一个支持高并发的抽奖系统的设计和实现为例来进行 Go 语言的实战学习。

要实现一个抽奖系统,我们首先需要一个比较合理的抽奖算法,保证每个人抽到奖品的概率是一致的,同时又需要避免奖品被很快抽完的情况。所以本文的第一部分会用三种思路来讲解如何去实现一个合理的抽奖算法。

要具备高并发的能力,Go 的 http 组件是支持高并发能力的(底层由 Go 的协程支持),但是数据存储层也需要支持高并发、高吞吐量的存储系统,mysql 肯定是支持不了的,所以这里采用 Redis 实现。所以本文第二部分会介绍如何使用 Go 去操作 Redis。

抽奖系统中,高并发请求下,很容易出现两个线程或者协程同时对共享内存中的数据进行更改的问题。所以为了保证数据的原子性,我们对数据的操作都是以 CAS 的方式。Redis 中可以实现对数据的原子增减,但是无法实现数据的翻倍或者其他改变。为了支持所有场景,同时为了学习知识,这里我们借用 Redis 的事务来实现 Redis 的 CAS。所以第三部分主要介绍 Go 操作 Redis 来实现 cas

整个抽奖系统概览如下,本章 chat 主要阐述其中的部分实现核心点,包括:使用 Go 实现抽奖算法,Go 操作 Redis ,以及通过 Redis 实现 cas

Go 实现抽奖算法

抽奖算法是决定一个抽奖系统是否成功的关键要素之一

我们先来假设一下场景: 假如目前公司需要举行一个活动,一共有A、B、C 三种奖品。奖品数量分别为 200、2000、5000、需要在 6.1 ~ 6.3 这三种奖品发完。

最容易想到的一种做法是对 奖品 A、B、C 分别设置一个概率,每个用户根据设置好的概率进行派奖。但仔细想想这样会存在一个问题:每个活动我们对于抽奖的人数预测都是未知的。当抽奖人数多或者概率很高时,奖品很容易在很短的时间内被发完。当抽奖人数少或者概率很低时,奖品很有可能到最后还有很多都没发完… 所以完全凭借设置抽奖概率来抽奖是不太靠谱的。

为了避免上述情况,于是我们开始思考,既然抽奖的人数是未知的,我们无法控制抽奖人数,那能不能换个思路,我们来控制奖品的发放时间呢?在上面假设的场景中,我们分别有 A、B、C 三种奖品 200、2000、5000 个。需要在 6.1 日 0:00 ~ 6.3 日 24:00 点这段期间进行抽奖。也就是说一共有 7200 奖品,在 72 个小时内抽完。假如我们把这 72 个小时按照时间片进行分割,是不是就能防止奖品被抽完或者过剩太多的情况了?

由公式

ΔΔ t = endTime−startTimeawardAmountendTime−startTimeawardAmount 计算可以得出时间间隔为 1/100 小时,也就是 36s 随机发一个奖品

此时,在 ΔΔ t 时间内 A 被抽中的概率是 200 / (200 + 2000 + 5000) = 1/36 , B 被抽中的概率是 2000 / (200 + 2000 + 5000) = 10/36 ,C 被抽中的概率是 5000 / (200 + 2000 + 5000) = 25 /36

算法的基本思路确定后,怎么去实现呢?

我们将奖品分成 7200 份之后,每一个奖品都会有一个抽奖时间段,在这个时间段内奖品会在某一个时间点进行发放。假如用户抽奖的时间点在这个时间点之前,因为奖品没有发放,所以无法中奖。在这个时间点之后,则有概率获得奖品,这个概率可以由用户进行配置。假如有一个用户抽中了奖品,则其他用户在这个时间段内再也无法抽中奖品了。

思路一

第一种思路就是通过结构化数据库去实现,例如 mysql

这里需要建立三张表

  • 奖品信息表 : award_info

    • 奖品记录表 : award_record

    活动开始前,需要根据奖品信息表 award_info 里面的数据初始化奖池表 award_pool。用户抽奖时通过以下 sql 进行抽奖

select id from award_pool where award_release_time <&#61; now() and award_release_num > 0 limit 1

如果能够查询到数据&#xff0c;则执行更新 update 语句&#xff0c;否则抽奖失败

update award_pool set award_release_num &#61; award_release_num - 1 where id &#61; #{id} and award_release_num > 0

如果 update 执行成功&#xff0c;用户中奖&#xff0c;写中奖纪录到 award_record 表&#xff0c;否则抽奖失败

思路二

在思路一中&#xff0c;在抽奖之前&#xff0c;我们对需要初始化 awardpool 这张表来进行发奖。抽奖时&#xff0c;需要执行一条 select 语句 和 一条 update 语句。这种做法&#xff0c;在奖品量非常多时&#xff0c;初始化 awardpool 这张表会浪费很多时间&#xff0c;在用户并发请求量非常大&#xff0c;select 语句也会消耗很多性能。于是有了第二种思路&#xff0c;不进行奖品初始化操作&#xff0c;而是通过实时计算的方式进行抽奖。

计算奖品释放时间点 &#xff1a;

具体实现&#xff1a;

func GetAward(awardBatches []AwardBatch) (AwardBatch, error) {startTime , _ :&#61; ParseStringToTime(conf.Award.StartTime)endTime , _ :&#61; ParseStringToTime(conf.Award.EndTime)fmt.Println("startTime : ", startTime)fmt.Println("endTime : ", endTime)award , err :&#61; RandomGetAwardBatch(awardBatches)if err !&#61; nil {return AwardBatch{}, err}totalAmount :&#61; award.GetTotalAmount()totalBalance :&#61; award.GetTotalBalance()updateTime :&#61; award.GetUpdateTime()detaTime :&#61; (endTime - startTime) / totalAmountcurrentTime :&#61; time.Now().Unix()random :&#61; rand.New(rand.NewSource(updateTime))// 计算下一个奖品的释放时间releaseTime :&#61; startTime &#43; (totalAmount - totalBalance) * detaTime &#43; int64(random.Int()) % detaTimefmt.Println("releaseTime : " &#43; fmt.Sprintf("%d", releaseTime) &#43; " currentTime : " &#43; fmt.Sprintf("%d",currentTime))if (currentTime }func RandomGetAwardBatch(awardBatches []AwardBatch) ( AwardBatch , error ) {if len(awardBatches) &#61;&#61; 0 {return AwardBatch{} , errors.New("empty param awardBatches")}weight :&#61; int64(0)for _, awardBatch :&#61; range awardBatches {weight &#43;&#61; awardBatch.GetTotalBalance()}if weight &#61;&#61; 0 {return AwardBatch{}, errors.New("weight is 0")}random :&#61; rand.New(rand.NewSource(weight))num :&#61; random.Int63n(weight)for _, awardBatch :&#61; range awardBatches {num -&#61; awardBatch.GetTotalBalance()if num <0 {return awardBatch , nil}}return AwardBatch{}, errors.New("randomGetAwardBatch should shoot at least one batch")}

思路三

思路二相对于思路一来说&#xff0c;节省了初始化 award_pool 奖池 和 select 操作的时间。但还是会存在一个问题&#xff0c;由于采用的是关系型数据库 mysql 存储&#xff0c;所以能够支持并发数是有限的。当抽奖用户量很大时&#xff0c;数据库肯定是无法支撑的。所以为了能够支持高并发&#xff0c;我们需要摈弃 mysql &#xff0c;采取高性能、支持高并发量的 k/v 式数据库。这里我们使用 Redis

为了达到思路二类似的效果&#xff0c;我们设计两种数据结构 &#xff1a;

奖品种类、奖品总数和中奖时间采用 zset 进行存储&#xff0c;zset 中 value 为奖品名称&#xff0c;value 的分值 score 为奖品的中奖时间。通过 ZRANGE award_info 0 -1 WITHSCORES 即可得到所有奖品以及每种奖品的更新抽奖时间

奖品剩余数量采用哈希表进行存储&#xff0c;这样通过 HGETALL 即可查询到所有奖品以及奖品剩余数量

基于这种思路&#xff0c;我们就有了第三种代码实现如下&#xff08;这里只贴部分核心代码&#xff0c;完整代码请参考 GitHub&#xff09;

// 随机抽取一个奖品func RandomGetAwardBatch() \*AwardBatch {conn :&#61; RedisWrapper.GetConn()if conn &#61;&#61; nil {log.Println("conn is nil")return nil}defer conn.Close()retMap, err :&#61; Redis.Int64Map(conn.Do("HGETALL", getAwardBalanceKey()))if err !&#61; nil || retMap &#61;&#61; nil {log.Println("Redis HGETALL award error", err)return nil}totalBalance :&#61; int64(0)for _, value :&#61; range retMap {totalBalance &#43;&#61; value}fmt.Println("retMap : ", retMap)if totalBalance &#61;&#61; 0 {log.Println("total balance is 0")return nil}log.Println("totalBalance :", totalBalance)awardBatches :&#61; GetAllAwardBatch()for index , awardBatch :&#61; range awardBatches {awardBatches[index].totalBalance &#61; retMap[awardBatch.GetName()]}log.Println("awardBatches :", awardBatches)random :&#61; rand.New(rand.NewSource(totalBalance))num :&#61; random.Int63n(totalBalance)for _ , awardBatch :&#61; range awardBatches {// 奖品已经抽完if awardBatch.GetTotalBalance() <&#61; 0 {log.Println("奖品已经抽完")continue}num &#61; num - awardBatch.GetTotalBalance()if num <0 {return &awardBatch}}return nil}func GetAwardBatch() *AwardBatch {awardBatch :&#61; RandomGetAwardBatch()if awardBatch &#61;&#61; nil {log.Println("sorry, you didn&#39;t win the prize.")return nil}// 判断是否到达奖品释放时间点startTime , _ :&#61; ParseStringToTime(Conf.Award.StartTime)endTime , _ :&#61; ParseStringToTime(Conf.Award.EndTime)totalAmount :&#61; awardBatch.totalAmounttotalBalance :&#61; awardBatch.totalBalancelastUpdateTime :&#61; awardBatch.GetUpdateTime()random :&#61; rand.New(rand.NewSource(lastUpdateTime))detaTime :&#61; (endTime - startTime) / awardBatch.totalAmount// 计算下一个奖品释放的时间点releaseTime :&#61; startTime &#43; (totalAmount - totalBalance) * detaTime &#43; int64(random.Int()) % detaTimelog.Println("relaseTime : ", time.Unix(releaseTime, 0).Format("2006-01-02 15:04:05"))if time.Now().Unix()

以上就是三种实现抽奖算法的思路&#xff0c;以及抽奖算法的 Go 实现。对比思路一、二、三&#xff0c;最终我们决定使用思路三进行实现。在思路上中&#xff0c;牵涉到对 Redis 的操作&#xff0c;所以这里我们需要先了解使用 Go 如何进行 Redis 操作。

Go 操作 Redis

Redis 是一个开源的高性能 key-value 存储系统&#xff0c;被广泛运用于高并发读写请求的业务场景。对 Redis 不了解的同学可以先去熟悉一下&#xff1a;https://Redis.io/documentation

那么 Go 是如何支持 Redis 操作的呢&#xff1f; 这里就不得不提到 rediGo , GitHub 地址为 GitHub.com/Gomodule/rediGo 下面可能会用一个高并发抽奖系统的场景来具体讲解 Redis 的操作和 cas 的实现

rediGo是 Go 语言的一个 Redis 客户端实现

安装 rediGo

Go get GitHub.com/garyburd/rediGo/Redis

对一个第三方包的了解首先从 GitHub 上开始&#xff0c;这里我们进入rediGo GitHub 官方 api https://Godoc.org/GitHub.com/Gomodule/rediGo/Redis 通过Godoc 文档我们很快可以快速上手

建立连接

首先来看官方 Godoc 介绍

ConnectionsThe Conn interface is the primary interface for working with Redis.Applications create connections by calling the Dial,DialWithTimeout or NewConn functions. In the future, functions will be added for creating sharded and other types of connections.

通过 Godoc 可知&#xff0c;Redis 建立连接有三种方式&#xff0c;Dial、DialWithTimeout、NewConn。

我们只需采用其中一种方式就可以快速建立连接 :

package Redis import ( "fmt" "GitHub.com/garyburd/rediGo/Redis" "log" ) // 返回一个 Redis 连接 func GetConn() Redis.Conn{conn , err :&#61; Redis.Dial("tcp", fmt.Sprintf("%s:%d", "127.0.0.1", 6379))if err !&#61; nil {log.Println("connect Redis error ", err)return nil}return conn}

需要注意的是&#xff0c;上述方法返回了一个 Redis 连接&#xff0c;取得这个连接之后&#xff0c;一定要记得关闭。如下 &#xff1a;

conn :&#61; getConn()if conn &#61;&#61; nil {log.Println("conn is nil")}defer conn.Close()

执行 Redis 命令

通过 Godoc 得知&#xff0c;我们有两种方式可以执行 Redis 命令

方式一 &#xff1a; 通过 Conn 接口的 Do(commandName string, args …interface{}) 方法

Do(commandName string, args ...interface{}) (reply interface{}, err error)例如 &#xff1a;n, err :&#61; conn.Do("APPEND", "key", "value")在抽奖系统中&#xff0c;我们需要通过读取 Redis 来获取所有奖品信息&#xff0c;代码如下&#xff1a;// 这里封装了一个方法 返回一个 Redis Connconn :&#61; RedisWrapper.GetConn()if conn &#61;&#61; nil {log.Println("conn is nil")return awardBatches}// 注意关闭 Redis conndefer conn.Close()// 获取存放奖品信息的 Redis keyawardInfoKey :&#61; getAwardInfoKey()// 调用 Do 方法读取对应的奖品信息 &#xff0c;通过 Redis.Values 方法转化为一个数组values , err :&#61; Redis.Values(conn.Do("ZRANGE",awardInfoKey,0,-1,"WITHSCORES"))

方式二 &#xff1a;通过 pipeline

Redis pipeline 可以一次性发送多条命令并在执行完后一次性将结果返回&#xff0c;pipeline 的底层实现是队列&#xff0c;所以是 FIFO&#xff0c;可以保证队列中命令执行的顺序性。

实现一个 pipeline 需要下面三个方法&#xff1a;

// 往队列中添加指令Send(commandName string, args ...interface{}) error// 一次性发送指令到服务端Flush() error// 接收服务端返回的数据Receive() (reply interface{}, err error)

例如在抽奖系统中&#xff0c;我们需要初始化奖品信息&#xff0c;此时我们使用的是 pipeline ,代码如下 &#xff1a;

func InitAwardPool() {// 这里封装了一个方法 返回一个 Redis Connconn :&#61; RedisWrapper.GetConn()if conn &#61;&#61; nil {log.Println("conn is nil")return}defer conn.Close()conn.Send("ZADD", getAwardInfoKey(), time.Now().Unix(), "A")conn.Send("ZADD", getAwardInfoKey(), time.Now().Unix(), "B")conn.Send("ZADD", getAwardInfoKey(), time.Now().Unix(), "C")conn.Send("HSET", getAwardBalanceKey(), "A", Conf.AwardMap["A"])conn.Send("HSET", getAwardBalanceKey(), "B", Conf.AwardMap["B"])conn.Send("HSET", getAwardBalanceKey(), "C", Conf.AwardMap["C"])conn.Flush()for i :&#61; 0 ; i <6; i&#43;&#43; {// 接收数据&#xff0c;这里不需要处理接收的数据&#xff0c;所以用 \__ , err :&#61; conn.Receive()if err !&#61; nil {log.Println("conn send error", err)}}}



实现 CAS

在高并发抽奖系统的场景中&#xff0c;假设我们有一个对当前奖品余量减少 1 的操作。 对奖品余量数的更改&#xff0c;我们希望应该是原子操作。更改包括两个操作&#xff1a;

a. 读出 Redis 余量数 b. 设置余量数为当前余量数 - 1

这里包括一读一写操作。 由于 Redis 是单进程&#xff0c;所以在进行单独读操作和单独写操作都是线程安全的。但是读 &#43; 写就不一定安全了。

这里举个例子&#xff1a;

假如 balance 为 200, 有 A、B 两个客户端请求&#xff0c;都需要进行 balance - 1 操作&#xff0c; 如下图 &#xff1a;

我们希望的结果是 A、B 都按照 读 —— 写 —— 读 —— 写的顺序执行&#xff0c;最终的结果是 198 。但由于 Redis 是单条指令执行&#xff0c;所以有可能 A 读 —— B 读 —— A 写 —— B 写 &#xff0c;这样的执行顺序&#xff0c;A、B 读取出来的 balance 都是 200 &#xff0c;A 执行写入 balance - 1 &#61; 199 , B 同时执行 balance - 1 &#61; 199 , 所以最终 balance &#61; 199 &#xff0c; 而不是我们预期的 198。

要避免上述情况&#xff0c;我们希望读 &#43; 写是一个原子操作。也就是说一定是 读 —— 写 —— 读 —— 写 这种顺序&#xff0c;读和写操作之间不能插入其他的读或者写操作。这其实就是一种 CAS 能力。但是 Redis 是没有提供这种能力的&#xff0c;这就需要我们自己基于 Redis 去实现 CAS

1. 什么是 CAS &#xff1f;

CAS 即 CompareAndSwap &#xff08;比较并替换&#xff09;的缩写

CAS 需要有3个操作数&#xff1a;内存地址 V&#xff0c;旧的-size-adjust:none;-webkit-font-smoothing:antialiased;box-sizing:border-box;margin:0px 0px 1.1em;outline:0px;">2. Redis 事务

要了解 Redis CAS 的实现&#xff0c;首先要了解以下 Redis 的事务机制。

事务一般有三个步骤 &#xff1a; begin、commit、rollback 。 begin 指示事务的开始&#xff0c;commit 指示事务的提交&#xff0c;rollback 指示事务的回滚

在 Redis 中对应的命令是 &#xff1a;multi、exec、discard。 multi 指示事务的开始&#xff0c;exec 指示事务的执行&#xff0c;discard 指示事务的丢弃

这里可以参考一下 Redis 的 文档 , 地址为 &#xff1a; http://Redisdoc.com/transaction/index.html

通过阅读文档可知&#xff0c; Redis 事务并没有实现原子性&#xff0c;即使事务中有某个/某些命令在执行时产生了错误&#xff0c; 事务中的其他命令仍然会继续执行。

例如 &#xff1a;

可以看到&#xff0c;在执行 incr award 这条指令的时候报错了&#xff0c;但是后面的 set book Golang 这条指令仍然能正确执行。

3. watch 命令

在了解了 Redis 的事务机制之后&#xff0c;我们发现 Redis 事务机制并不支持原子性。所以通过 Redis 事务还是无法实现 CAS &#xff0c;这里就需要引入 watch 命令了。

使用 watch 命令可以监控 Redis 的某个 key&#xff0c;假如在 Redis 事务执行 exec 过程中&#xff0c;这个 key 发生了变动&#xff0c;则该事务不会被执行。

为了演示 watch 命令&#xff0c;这里我们新启两个 Redis 客户端&#xff0c;如下&#xff1a;

客户端 1 &#xff1a;

hing:antialiased;box-sizing:border-box;margin:0px 0px 1.1em;outline:0px;">客户端 2 :

在客户端 1 执行完 watch watchedKey 和 multi 命令后&#xff0c;客户端 2 执行 set 命令&#xff0c;我们可以看到此时客户端 1 的 set 命令会执行失败。说明在事务执行过程中&#xff0c;假如 watch 的 key 有更改&#xff0c;则关于该事务会执行失败。

4. Redis CAS 实现

通过 Redis 事务和 watch 命令&#xff0c;我们可以通过 Redis 去实现 CAS&#xff0c;在抽奖系统中&#xff0c;我们也应用了 CAS&#xff0c;相关代码如下 &#xff1a;

// 更新 lastUpdateTime 和 balance&#xff0c; 如果更新不成功&#xff0c;视为抽奖失败conn :&#61; RedisWrapper.GetConn()if conn &#61;&#61; return nil}defer conn.Close()conn.Send("WATCH", getAwardBalanceKey())conn.Send("MULTI")conn.Send("ZADD",getAwardInfoKey(),time.Now().Unix(), awardBatch.GetName())conn.Send("HSET",getAwardBalanceKey(), awardBatch.GetName(), awardBatch.totalBalance - 1)conn.Send("EXEC")err :&#61; conn.Flush()if err !&#61; nil {log.Println("Redis error, " , err)return nil}


抽中奖品之后&#xff0c;我们需要对剩余奖品数进行更新操作&#xff0c;上面代码通过 watch 命令去监视了保存奖品余量的哈希表&#xff0c;假如对奖品剩余数进行更新操作的时候&#xff0c;有其他协程修改奖品剩余数&#xff0c;则事务执行失败。此抽奖过程没有完成&#xff0c;视为用户没有中奖。

结语

至此&#xff0c;我们已经实现了一个支持高并发的抽奖系统的核心代码&#xff0c;要实现一个抽奖系统&#xff0c;除了抽奖算法、使用 Redis 存储数据以及 CAS 之外&#xff0c;还需要会使用 Go 的基本语法&#xff0c;了解 Go 的 http 模块的使用、log 的使用、第三方包的使用、配置文件的解析等。其实总的代码并不复杂&#xff0c;代码量也不多&#xff0c;所以其他部分读者可以自行去研究一下。欢迎读者圈交流


欢迎关注我的公众号&#xff0c;回复关键字“Golang” &#xff0c;将会有大礼相送&#xff01;&#xff01;&#xff01; 祝各位面试成功&#xff01;&#xff01;&#xff01;


推荐阅读
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 微软头条实习生分享深度学习自学指南
    本文介绍了一位微软头条实习生自学深度学习的经验分享,包括学习资源推荐、重要基础知识的学习要点等。作者强调了学好Python和数学基础的重要性,并提供了一些建议。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • EPICS Archiver Appliance存储waveform记录的尝试及资源需求分析
    本文介绍了EPICS Archiver Appliance存储waveform记录的尝试过程,并分析了其所需的资源容量。通过解决错误提示和调整内存大小,成功存储了波形数据。然后,讨论了储存环逐束团信号的意义,以及通过记录多圈的束团信号进行参数分析的可能性。波形数据的存储需求巨大,每天需要近250G,一年需要90T。然而,储存环逐束团信号具有重要意义,可以揭示出每个束团的纵向振荡频率和模式。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 本文分享了一个关于在C#中使用异步代码的问题,作者在控制台中运行时代码正常工作,但在Windows窗体中却无法正常工作。作者尝试搜索局域网上的主机,但在窗体中计数器没有减少。文章提供了相关的代码和解决思路。 ... [详细]
  • Centos7.6安装Gitlab教程及注意事项
    本文介绍了在Centos7.6系统下安装Gitlab的详细教程,并提供了一些注意事项。教程包括查看系统版本、安装必要的软件包、配置防火墙等步骤。同时,还强调了使用阿里云服务器时的特殊配置需求,以及建议至少4GB的可用RAM来运行GitLab。 ... [详细]
  • 如何使用Java获取服务器硬件信息和磁盘负载率
    本文介绍了使用Java编程语言获取服务器硬件信息和磁盘负载率的方法。首先在远程服务器上搭建一个支持服务端语言的HTTP服务,并获取服务器的磁盘信息,并将结果输出。然后在本地使用JS编写一个AJAX脚本,远程请求服务端的程序,得到结果并展示给用户。其中还介绍了如何提取硬盘序列号的方法。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
author-avatar
用户19910071
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有