目录:
?了解系统的功能、背景、场景及项目要求?在架构角度思索系统可能面临的问题以及解决方案?学习本项目所涉及的中间件等基础知识?能够从0搭建springcloud微服务系统框架?能够完成编码,使用中间件完成系统的业务代码?学会部署上线,学会基于jenkins+dockerswarm实现微服务的持续集成与动态扩容
1.1项目概述1.1.1概述假设公司要开年会,让你设计一套红包雨项目,在某段时间内随机发放不同的礼品,你该如何设计呢?
本项目实现了一个完整的红包雨模式抽奖系统,包括管理后台与前端界面。
由管理后台配置相关活动和奖品等信息,前端用户通过参与活动,完成抽奖。
1.1.2背景1.电商活动
互联网的发展中,电商是典型的应用场景。电商,即买卖,就比如要抓住消费者的心理。那么各种促销、活动、成为电商公司必备的业务模块,尤其C端业务,面向普通大众用户。在所有活动中,抽奖是最典型的一种。
1.红包雨
阿里春节的红包雨大家都多有参与。很多公司争相模仿,以实现随机派发红包的形式完成宣传与促销。红包雨可以理解为抽奖的一种特殊形式,奖品即红包。
1.企业年会
年会基本是每个公司绕不开的话题。公司年会中的抽奖环节更是必不可少。尤其互联网公司,线上抽奖基本成为大家约定俗成的形式。打开手机,参与抽奖。正是本项目所涉及的现实应用场景。
1.1.3系统要求1.并发性
抽奖系统比如涉及到访问量大的问题。系统涉及所面临的第一关,即活动开始的瞬间,大批用户点击的涌入。怎样设计系统以达到如此高并发情况下的及时响应是本项目的重中之重。
1.库存控制
抽奖面临的必然是奖品。数量控制是必须要做到精准吻合。不允许出现设置了5个奖品,最终6人中奖这种类似的问题出现。其中的本质是奖品库存的控制。后续的章节中会详细介绍本项目中如何做到控制库存的。
1.投放策略
在活动时间段内,管理员设置好的一堆奖品如何投放?红包何时出现?年会奖品什么时候可以被抽中?这些都涉及到投放策略。
本项目中会给大家展示最常见的一种策略,即在活动时间内,奖品随机出现。最后的课程会给出引申,如何灵活扩展实现其他的投放算法。
1.边界控制
活动何时开始?何时结束?倒计时如何控制。这涉及到活动的边界。开始前要提防用户提前进入抽奖。结束后要即使反馈结果给用户,告知活动已结束。
1.活动自由配置
活动的配置由后台管理员完成,可以自由配置活动的开始结束时间,主题、活动简介、有哪些奖品、不同等级的用户中奖的策略。这就要求系统必须具备足够的业务灵活度。
1.中奖策略
每个用户参与抽奖后,要遵从后台管理员所设定的中奖策略,典型的场景是针对用户设置最大中奖数。一旦用户中奖后,要进入计数,达到最大中奖数后,即使活动未结束,用户继续参与,也不能再让其中奖。而是将奖品机会倾向于其他参与者。下面的章节中会为大家展示如何根据后台策略精确控制用户中奖数量。
1.2功能展示1.2.1管理后台1)会员管理功能:用户查询、用户新增、删除、修改密码
用户管理为管理员提供基本的用户录入。本项目以企业年会为背景,可参与抽奖的用户由管理员后台直接录入,不允许私自注册其他非法账号。已录入的账号可以在抽奖前端页面中登录,参与抽奖。
在电商面向C端用户的情况下,新增一个注册接口,允许用户自行注册参与抽奖。
2)会员等级功能:等级新增、删除、编辑
不同等级的会员有不同的中奖策略设置。比如高级别的会员中奖次数更多。详细会涉及下面活动配置中策略配置一节
3)活动管理基础信息配置
功能:新增活动,修改活动,删除活动,配置活动基本信息(开始结束时间,标题,说明)
活动的基本信息管理功能
策略配置
功能:新增,修改,删除策略
策略涉及到用户的中奖次数,可以为不同等级的用户设置不同的最大中奖机会。不设置或者设置为0表示次数不限。
奖品配置
功能:添加,删除,编辑奖品
为活动配置响应的奖品,可以添加多个不同的奖品,并为每个奖品设置单独的数量。
4)奖品管理奖品管理
功能:奖品增加,编辑,删除
录入奖品的基本信息,可以供多个活动引用。
5)信息管理中奖统计
功能:只有按条件查询,不涉及其他操作
统计每个活动的奖品总数,以及被抽走的数量。该功能只涉及数据的统计,不涉及新增修改删除,属于只读操作。
中奖列表
功能:基于各种条件查询中奖详情
可以根据所需条件,查询到相关的中奖信息,中奖人信息,奖品信息,中奖时间等。该功能只涉及数据的统计,不涉及新增修改删除,属于只读操作。
1.系统管理
操作日志
功能:查询管理员的操作日志
该功能用于记录管理员的操作。可以根据ip,操作时间内容,以及操作人查询到在后台中的行为。只涉及数据的统计,不涉及新增修改删除,属于只读操作。
1.2.2前台展示1)活动列表2)活动详情3)抽奖展示4)个人中心1.3中间件介绍1.3.1redis1)简介Redis是当前比较热门的NOSQL系统之一,它是一个开源的使用ANSIc语言编写的key-value存储系统。
2)应用场景1.缓存,这是Redis当今最为人熟知的使用场景。在提升服务器性能方面非常有效;2.排行榜,使用传统的关系型数据库(mysqloracle等)来做这个事,非常的麻烦,而利用Redis的zset(有序集合)数据结构能够简单的搞定;3.计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力,在本项目中会用到该功能;4.好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便解决一些共同好友、共同爱好之类的功能;5.简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;6.Session共享,借助spring-session,后端用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。7.热数据查询,一些频繁被访问的数据,经常被访问的数据如果放在关系型数据库,每次查询的开销都会很大,而放在redis中,因为redis是放在内存中的,会得到量级的提升。
3)数据类型hset:key-field-value结构,用于一组相似类别的k-v值存储。类似java中的hashset工具
list:队列,可以实现左右进出操作。类比java中的LinkedList,本项目中的抽奖令牌桶,使用的就是list
kv:最典型的缓存存储结构,value可以存储对应的对象。
zset:有序集合,在排序类,例如搜索词排名场景中会用到。
4)相关命令对KEY操作的命令
exists(key):确认一个key是否存在
del(key):删除一个
keytype(key):返回值的类型
keys(pattern):返回满足给定pattern的所有key
randomkey:随机返回key空间的一个
keyrename(oldname,newname):重命名key
dbsize:返回当前数据库中key的数目
expire:设定一个key的活动时间(s)
ttl:获得一个key的活动时间
move(key,dbindex):移动当前数据库中的key到dbindex数据库
flushdb:删除当前选择数据库中的所有key
flushall:删除所有数据库中的所有key
对String操作的命令
set(key,value):给数据库中名称为key的string赋予值value
get(key):返回数据库中名称为key的string的value
getset(key,value):给名称为key的string赋予上一次的value
mget(key1,key2,…,keyN):返回库中多个string的value
setnx(key,value):添加string,名称为key,值为value
setex(key,time,value):向库中添加string,设定过期时间time
mset(keyN,valueN):批量设置多个string的值
msetnx(keyN,valueN):如果所有名称为keyi的string都不存在
incr(key):名称为key的string增1操作
incrby(key,integer):名称为key的string增加integer
decr(key):名称为key的string减1操作
decrby(key,integer):名称为key的string减少integer
append(key,value):名称为key的string的值附加value
substr(key,start,end):返回名称为key的string的value的子串
对List操作的命令
rpush(key,value):在名称为key的list尾添加一个值为value的元素
lpush(key,value):在名称为key的list头添加一个值为value的元素
llen(key):返回名称为key的list的长度
lrange(key,start,end):返回名称为key的list中start至end之间的元素
ltrim(key,start,end):截取名称为key的list
lindex(key,index):返回名称为key的list中index位置的元素
lset(key,index,value):给名称为key的list中index位置的元素赋值
lrem(key,count,value):删除count个key的list中值为value的元素
lpop(key):返回并删除名称为key的list中的首元素
rpop(key):返回并删除名称为key的list中的尾元素
blpop(key1,key2,…keyN,timeout):lpop命令的block版本。
brpop(key1,key2,…keyN,timeout):rpop的block版本。
对Set操作的命令
sadd(key,member):向名称为key的set中添加元素member
srem(key,member):删除名称为key的set中的元素
memberspop(key):随机返回并删除名称为key的set中一个元素
smove(srckey,dstkey,member):移到集合元素
scard(key):返回名称为key的set的基数
sismember(key,member):member是否是名称为key的set的元素
sinter(key1,key2,…keyN):求交集
sinterstore(dstkey,(keys)):求交集并将交集保存到dstkey的集合
sunion(key1,(keys)):求并集
sunionstore(dstkey,(keys)):求并集并将并集保存到dstkey的集合
sdiff(key1,(keys)):求差集
sdiffstore(dstkey,(keys)):求差集并将差集保存到dstkey的集合
smembers(key):返回名称为key的set的所有元素
srandmember(key):随机返回名称为key的set的一个元素
对Hash操作的命令
hset(key,field,value):向名称为key的hash中添加元素field
hget(key,field):返回名称为key的hash中field对应的value
hmget(key,(fields)):返回名称为key的hash中fieldi对应的value
hmset(key,(fields)):向名称为key的hash中添加元素field
hincrby(key,field,integer):将名称为key的hash中field的value增加integerhexists(key,field):名称为key的hash中是否存在键为field的域
hdel(key,field):删除名称为key的hash中键为field的域
hlen(key):返回名称为key的hash中元素个数
hkeys(key):返回名称为key的hash中所有键
hvals(key):返回名称为key的hash中所有键对应的value
hgetall(key):返回名称为key的hash中所有的键(field)及其对应的value
5)三种模式主从复制:主从模式指的是使用一个redis实例作为主机,其余的实例作为备份机。主机和从机的数据完全一致,主机支持数据的写入和读取等各项操作,而从机则只支持与主机数据的同步和读取。主从模式很好的解决了数据备份问题,并且由于主从服务数据几乎是一致的,因而可以将写入数据的命令发送给主机执行,而读取数据的命令发送给不同的从机执行,从而达到读写分离的目的。
哨兵:哨兵模式是一种特殊的模式,哨兵是一个独立的进程,独立运行。
当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。做到了主从自动切换和高可用。
集群:RedisCluster是一个高性能高可用的分布式系统。由多个Redis实例组成的整体,数据按照Slot存储分布在多个Redis实例上,通过Gossip协议来进行节点之间通信。
Redis3.0之后版本支持。
客户端连任意节点。
6)持久化Redis支持RDB和AOF两种持久化机制,持久化功能有效地避免因进程退出造成的数据丢失问题,当下次重启时利用之前持久化文件即可实现数据恢复。
rdb:
在指定时间间隔内,将内存中的数据集快照写入磁盘,也就是Snapshot快照,它恢复时是将快照文件直接读到内存中,来达到恢复数据的,rdb也是redis默认的持久化方式。
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写进一个临时文件中,等到持久化过程结束了,再用这个临时文件替换上次持久化好的文件。在这个过程中,只有子进程来负责IO操作,主进程仍然处理客户端的请求,这就确保了极高的性能。
优点
适合大规模数据恢复的场景,数据紧凑,易迁移
缺点RDB这种持久化方式数据完整性很难保证,虽然我们可以用过修改持久化的频率,但是如果还没有触发快照时,本机就宕机了,那么对数据库所做的写操作就丢失了。每次进行RDB时,父进程都会fork一个子进程,由子进程来进行实际的持久化操作,数据量大时,那么fork出子进程的这个过程将是非常耗时的。
aof:
以日志的形式记录Redis每一个写操作,将Redis执行过的所有写指令记录下来,注意,读操作是不需要记录的,redis启动之后会读取appendonly.aof文件,将之前的操作复现,来完成恢复数据的工作。
appendfsyncalways:每修改同步,每一次发生数据变更都会持久化到磁盘上,性能较差,但数据完整性较好。
appendfsynceverysec:每秒同步,每秒内记录操作,异步操作,如果一秒内宕机,有数据丢失。
appendfsyncno:不同步。
重写:当然如果AOF文件一直被追加,这就可能导致AOF文件过于庞大。因此,为了避免这种状况,Redis新增了重写机制,当AOF文件的大小超过所指定的阈值时,Redis会自动启用AOF文件的内容压缩,只保留可以恢复数据的最小指令集
优点
看上去轻量化,增量形式
保留了redis的历史操作
多种策略配置,相对灵活
缺点
对于相同的数据集来说,AOF文件要比RDB文件大。
根据所使用的持久化策略来说,AOF的速度要慢与RDB。
1.3.2zookeeper1)简介zookeeper是一个分布式服务框架,是ApacheHadoop的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
2)节点类型临时节点:临时节点的生命周期和客户端会话绑定在一起,客户端会话失效,则这个节点就会被自动清除。
永久节点:该数据节点被创建后,就会一直存在于zookeeper服务器上,直到有删除操作来主动删除这个节点。
3)使用场景配置中心:配置中心,顾名思义就是将配置数据写到ZK节点上,供各个分布式机器获取配置,同时监听自己对应的节点。实现配置信息的集中式管理和动态更新。
命名服务:在分布式系统中,通过使用命名服务,客户端应用能够根据指定名字来获取资源或服务的地址,提供者等信息。被命名的实体通常可以是集群中的机器,提供的服务地址,远程对象等等,这些我们都可以统称他们为名字(Name)。通过调用ZK提供的创建节点的API,能够很容易创建一个全局唯一的path,这个path就可以作为一个名称。
分布式通知:ZooKeeper的watcher注册与异步通知机制,能够很好的实现分布式环境下不同系统之间的通知与协调,实现对数据变更的实时处理。不同系统都对ZK上同一个znode进行注册,监听znode的变化(包括znode本身内容及子节点的),其中一个系统update了znode,那么另一个系统能够收到通知,并作出相应处理
选主:利用ZooKeeper的一致性,能够保证在分布式高并发情况下节点创建的全局唯一性,即:同时有多个客户端请求创建/currentMaster节点,最终一定只有一个客户端请求能够创建成功。利用这个特性,就能很轻易的在分布式环境中进行集群选取了。
分布式锁:分布式锁,这个主要得益于ZooKeeper的节点创建和事件监听机制。
4)相关命令创建节点create列出节点ls获取节点信息get检查状态stat修改节点set删除节点rmr删除节点delete
5)高可用集群与选主:以5台机器启动时场景为主,过程如下:
1.服务器1启动,此时只有它一台服务器启动了,它发出去的报没有任何响应,所以它的选举状态一直是LOOKING状态。2.服务器2启动,它与最开始启动的服务器1进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以id值较大的服务器2胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是3),所以服务器1,2还是继续保持LOOKING状态。3.服务器3启动,根据前面的理论分析,服务器3成为服务器1,2,3中的老大,而与上面不同的是,此时有三台服务器选举了它,所以它成为了这次选举的leader。4.服务器4启动,根据前面的分析,理论上服务器4应该是服务器1,2,3,4中最大的,但是由于前面已经有半数以上的服务器选举了服务器3,状态是following,所以它只能接收当小弟的命了。5.服务器5启动,同4一样,当小弟。
1.3.3rabbitmq1)简介RabbitMQ是一个由erlang开发的AMQP(AdvancedMessageQueue)的开源实现
2)模块介绍Broker:可以简单理解为一台物理机器。
Producer:消息生产者,就是投递消息的程序。
Consumer:消息消费者,就是接受消息的程序。
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。fanout,direct,topic,header
Queue:消息的载体,每个消息都会被投到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
RoutingKey:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离。(不涉及)
3)应用消息传递、异步处理、应用解耦、流量削峰
4)高可用发送方:confirm机制(发送成功后有异步通知)
消费端:ACK消息应答机制
rabbit:queue持久化,消息持久化(deliveryMode=2)
1.3.4nginxNginx是一款轻量级的Web服务器/反向代理服务器及电子邮件(IMAP/POP3)代理服务器,在BSD-like协议下发行。其特点是占有内存少,并发能力强,事实上nginx的并发能力在同类型的网页服务器中几乎成为公认的标杆,在百度、京东、新浪、网易、腾讯、淘宝等互联网公司中均有应用。
1)动静分离静态资源:由nginx作为web服务器身份,直接返回
动态资源:nginx将请求转发出去,交给后端应用服务器处理
2)负载均衡当网站的访问量达到一定程度后,单台服务器不能满足用户的请求时,需要用多台服务器集群提升并行处理能力。并且多台服务器可以平均分担负载,不会因为某台服务器负载高宕机而某台服务器闲置的情况。这时使用nginx实现了机器之间的负载均衡。
3)配置介绍location
proxy_pass
upstream
2.系统设计2.1建模2.1.1ER图2.1.2数据表1)奖品表card_product
字段类型备注idint(10)unsignednamevarchar()奖品名称picvarchar()图片infovarchar()简介pricedecimal(10,2)市场价2)活动表card_game
字段类型备注idint(10)unsignedtitlevarchar()活动主题infovarchar()活动简介starttimedatetime开始时间endtimedatetime结束时间typetinyint(2)类型(1=概率类,2=随机类)statustinyint(1)状态(0=新建,1=已加载)3)会员表card_user
字段类型备注idint(11)unsignedunamevarchar(20)用户名passwdvarchar(50)密码realnamevarchar(10)姓名idcardvarchar(18)身份证号phonevarchar(15)手机号码levelsmallint(6)等级createtimedatetime注册时间updatetimedatetime更新时间4)策略表card_game_rules
字段类型备注idint(11)unsignedgameidint(11)unsigned活动iduserlevelsmallint(6)会员等级enter_timessmallint(6)可抽奖次数(0为不限)goal_timessmallint(6)最大中奖次数(0为不限)5)中奖纪录card_user_hit
字段类型备注idint(10)unsignedgameidint(10)unsigned活动useridint(10)unsigned用户productidint(10)unsigned奖品hittimedatetime中奖时间6)奖品活动关联关系card_game_product
字段类型备注idint(10)unsignedgameidint(11)unsigned活动idproductidint(11)奖品idamountsmallint(6)数量2.1.3视图1)中奖信息view_card_user_hit
字段类型备注idint(10)unsignedtitlevarchar()活动主题typevarchar()值unamevarchar(20)用户名realnamevarchar(10)姓名idcardvarchar(18)身份证号phonevarchar(15)手机号码levelvarchar()值namevarchar()奖品名称pricedecimal(10,2)市场价gameidint(10)unsigned活动useridint(10)unsigned用户productidint(10)unsigned奖品hittimedatetime中奖时间2)奖品数统计view_game_curinfo
字段类型备注idint(10)unsignedtitlevarchar()活动主题starttimedatetime开始时间endtimedatetime结束时间typevarchar()值totaldecimal(27,0)hitbigint(21)2.2概要设计2.2.1系统拓扑1)业务架构2)软件架构2.2.2设计原则1)动静分离1·静态文件分离,nginx直接响应,不要再绕后台应用机器
2)微服务化1·将模块细粒度拆分,微服务化
2·借助dockerswarm的容器管理功能,实现不同服务的副本部署,滚动更新
3·在本项目中,api模块就部署了3份,以适应前端的高并发
3)负载均衡1·多个实例之间通过nginx做负载均衡,提升并发性能
2·本项目为大家展示的模块均部署在1台节点。生产环境涉及多台机器,用upstream实现。
4)异步消息1·中奖后,中奖人及奖品信息要持久化到数据库。引入rabbitmq,将抽奖操作与数据库操作异步隔离。
2·抽奖中奖后,只需要将中奖信息放入rabbitmq,并立即返回中奖信息给前端用户。
3·后端msg模块消费rabbitmq消息,缓慢处理。
5)缓存预热1·每隔1分钟扫描一次活动表,查询未来1分钟内将要开始的活动。
2·将扫到的活动加载进redis,包括活动详细信息,中奖策略信息,奖品信息,抽奖令牌。
3·活动正式开始后,基于redis数据做查询,不必再与数据库打交道。
2.2.3交互序列图2.2.4缓存体系缓存体系概览图:
1)活动基本信息k-v,以活动id为key,活动对象为value,永不超时
redisUtil.set(RedisKeys.INFO+game.getId(),game,-1);2)活动策略信息
hset,以活动id为group,用户等级为key,策略值为value
redisUtil.hset(RedisKeys.MAXGOAL+game.getId(),r.getUserlevel()+"",r.getGoalTimes());redisUtil.hset(RedisKeys.MAXENTER+game.getId(),r.getUserlevel()+"",r.getEnterTimes());3)抽奖令牌桶
双端队列,以活动id为key,在活动时间段内,随机生成时间戳做令牌,有多少个奖品就生成多少个令牌。令牌即奖品发放的时间点。从小到大排序后从右侧入队。
redisUtil.rightPushAll(RedisKeys.TOKENS+game.getId(),tokenList);4)奖品映射信息
k-v,以活动id_令牌为key,奖品信息为value,会员获取到令牌后,如果令牌有效,则用令牌token值,来这里获取奖品详细信息
redisUtil.set(RedisKeys.TOKEN+game.getId()+"_"+token,productMap.get(cgp.getProductid()),expire);5)令牌设计技巧
假设活动时间间隔太短,奖品数量太多。那么极有可能产生的时间戳发生重复。
解决技巧:额外再附加一个随机因子。将(时间戳*+3位随机数)作为令牌。抽奖时,将抽中的令牌/,还原真实的时间戳。
//活动持续时间(ms)longduration=end-start;longrnd=start+newRandom().nextInt((int)duration);//为什么乘,再额外加一个随机数呢?-防止时间段奖品多时重复longtoken=rnd*+newRandom().nextInt();6)中奖计数
k-v,以活动id_用户id作为key,中奖数为value,利用redis原子性,中奖后incr增加计数。
redisUtil.incr(RedisKeys.USERHIT+gameid+"_"+user.getId(),1);7)中奖逻辑判断
抽奖时,从令牌桶左侧出队和当前时间比较,如果令牌时间戳小于等于当前时间,令牌有效,表示中奖。大于当前时间,则令牌无效,将令牌还回,从左侧压入队列。
2.3框架选型2.3.1管理后台借助开源zcurd开发平台,完成后台基本的增删改查。
2.3.2前台模块基于springcloud构建微服务体系
1)公共pom所有项目共用的依赖及版本定义。继承自springboot2.1.7.RELEASE,依赖cloudGreenwich.SR2
parentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.1.7.RELEASE/version/parent
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-dependencies/artifactIdversionGreenwich.SR2/versionscopeimport/scopetypepom/type/dependency2)公共模块