政资汇项目总结

1- 网站架构

1台Nginx做前置的负载均衡器(keepalive再保持一个备用Nginx) + 3台web服务器(Tomcat web server),每台web服务器都有一个Redis只读K-V内存服务器(设置的内存较小避免web服务器竞争内存,一级堆外缓存) + 1台远程二级Redis主缓存(大内存,设置1Master-1Slave模式,AOF转发,基于Sentinel + master/slave + VIP漂移来做高可用和故障切换)+ Mysql数据库(1Maste-for-write/read, 2Slave-for-read)

2- 架构需要解决的问题

1. 负载均衡器引入出现的问题:

Session在不同web服务器中的问题:HTTP协议本身无状态,需要基于HTTP协议支持会话状态保持机制——session

拓展:什么是Session,session的实现原理,session与Cookie的区别于联系,session在 Tomcat中是如何存储的,在什么时候初始化session的,等等问题

解决方案:

  • session sticky(让同一用户的请求落在同一台web服务器上)优缺点
  • session replication(每台web服务器都保存所有的session数据)优缺点
  • 集中session存储方案,将session保存到远程Redis或者数据库中 优缺点
  • Cookie Based基于Cookie来实现Session 优缺点

总结:Session sticky和集中式存储是比较好的解决方案,即使这两种方案都有缺点

本系统架构采用的方案:
负载均衡使用Hash(源IP转换成二进制,然后高位和低位做异或操作)%web服务器数量方式(Nginx可以通过Lua脚本来做自定义的负载均衡策略),能保证web服务器的均衡以及粘性Session。但是面临的问题是web服务器挂了,会丢失session。

2. Mysql读写分离以及Master-Slave模式下面临的问题

Mysql主从备份不支持实时同步,而是异步的,所以主库和备库之间存在延迟,所以会面临数据暂时不一致的问题,以及读写代理问题。

Spring读写分离配置,配置多个数据源,然后根据sql语句以及事务进行sql分发,
也可采用amoeba单独部署Mysql代理服务器,Spring只需要配置一个数据源到amoeba即可。

解决方案:

  • 一致性:写操作和事务中的读写都要走主库,非事务只读操作走备库(将写操作和事务添加到amoeba的代理上)。搜索引擎在备库上进行只读操作,减少主库的压力
  • 读写代理:amoeba进行读写分离代理。或者配置Spring和Mybatis进行读写分离,一主多从读库时还需对读库进行动态选择
3. 如果数据库性能还是有瓶颈则使用Redis来做缓存

可以将数据库中查询出来的数据做缓存,也可以将静态资源进行缓存,如html、js、css等,图片等资源也可以使用单独的静态服务器,并使用CDN将静态内容缓存到网络运行商的节点。

两种缓存:

  • 一种是上述架构中的一级堆外本地缓存,只读缓存,不需要保证高可用但是内存设置要小一点,防止和web服务器过度竞争内存资源
  • 一种是上述架构的二级远程读写缓存,可读写,需要进行持久化以及高可用,内存较大,一般用来放置任务队列,保证任务不丢失,和占用大内存的数据集,如结果集很大的搜索结果等,开辟一个数据同步的线程,将需要持久化的执行结果写到数据库中(write behind cached模式)。

哪些需要缓存

将比较热的,而且是读多写少的数据缓存(网站应用遵从二八定律,80%的操作在读操作上,数据分布上,80%的操作集中到20%的数据上),读少写多的数据每次都将直接命中后台数据库,所以不用缓存(也可使用 write behind cached模式缓存读少写多的数据,但是不能保证数据的可靠性,一旦系统崩溃或者掉电见丢失数据)

数据载入过程:

缓存的填充是应用来完成的,即应用访问一级缓存,如果数据不存在,则再将操作代理到二级缓存上,如果查到则立即返回,如果没有则命中数据库,将数据库中的数据填充到一级缓存中,然后提交一个载入数据的任务给二级缓存的载入程序,这个程序会将之前执行的sql语句进行重新执行并将结果载入到二级缓存中。从中可以发现如果一级二级缓存都没有命中将增加数据库一倍的读压力。

使用缓存关注的两点:

  • 缓存数据更新的一致性策略
  • 缓存达到设置的上限,新插入缓存如何替换旧缓存的策略

使用缓存遇到的问题:

缓存击穿、缓存颠簸、缓存雪崩,并发更新缓存、热点Key等一系列问题及解决方案

4. 增加缓存后,数据库的性能还是应用的瓶颈那么就分库分表吧

注意:一般中小型网站数据库还没有大到要分库分表的地步,本系统架构并没有分库分表

为什么要拆分:

  • 读写分离是降低主库的读压力,但是主库的写压力还在,所以分库分表就是为了解决主库的写压力
  • Spring配置多个数据源后,还需要将Dao层上注解指定的数据源(因为不像读写分离一样,每个数据源数据都一样,拆分后数据分布到不同数据源中,需手动指定数据源)

分库和分表都有两种方式:水平拆分和垂直拆分

  • 垂直拆分,将一个数据库中不同业务的表table拆分到多个数据库中,专库专用
    面临的问题:处理跨业务的事务
    解决方案:

    • 使用分布式事务(拓展知识:分布式事务的实现方式,两阶段、三阶段提交CAP/BASE理论等等)
    • 不使用强事务,而采用补偿式
  • 水平拆分,将同一数据库中的同一张表table,拆分到多个数据库中(每个数据库中保存原表的一部分数据);也有两种拆分方式:按hash(id)%n,以及按range:1-1000存在机器1上,1001-2000存放在机器2上,依次类推。这两方式各有优缺点特别的对于按range拆分,对于用户表,一般新注册的用户具有更高的活跃度,使得range尾部的机器压力变大
    面临的问题1:水平拆分后如何根据id定位到数据库的位置
    面临的问题2:同一张表在不同数据库中如何保证Id的唯一性
    面临的问题3:一张表在不同数据库之间的统计操作

    解决方案1:
    1 根据hash(id)%n的方式定位
    2 根据外部维护的范围区间映射表来定位

    解决方案2:
    1 中央发号器:通过锁来控制取区间操作,然后一次去一段去使用
    2 本地生成:UUID+MAC地址+端口然后取64位
    3 拓展:Unique ID需要满足的性质

    解决方案3:
    1 分别统计,在应用层进行合并

2- 用户管理业务模块

1. 用户注册模块
  • 分为普通企业用户(不需特别身份证明)
  • 政府资助项目联系人(需要身份证明,防止假传消息,需要提供部门信息,部门负责人信息,后台人员进行核实,展示时会有特殊身份标示)
    注册时将密码进行MD5(password + 随机salt)进行加密,然后取16位作为密码,然后将密码和随机salt都存进数据库中
2. 用户登录模块

任何需要交互的地方都需要保证是登录状态,用来记录用户的行为

  • 登录验证,有token进行token验证,没有token则跳转到登录页面,进行邮箱/手机号和密码的校验(前端+后端双校验)
  • 单点登录问题:发放token,以及对token进行缓存,设置失效时间,当一个用户在不同IP登录时,先删除旧的token,然后在设置新的token
3. 用户中心
  • 个人中心:账号设置(密码,邮箱,手机号,邮件通知选项),系统通知,我的分享,我的回复(别人回复我,我回复别人,会跟上对应的分享信息),朋友私信
    关注列表(取消关注,发送私信等,注意关注的人可能是项目联系人也可能是其他企业用户)
  • 通知:(1)系统通知:异常登录通知,关注的领域有新动态的通知。(2)互动通知:朋友私信,被收藏,被赞,分享的信息有新的回复,评论有新的回复
4. 异步任务功能实现

将功能拆分开,一个业务功能可能包含一个强一致的部分,这个部分没有办法做优化,还会包含一致性不强的部分,将这部分使用异步任务队列的方式能减少业务处理时间,更何况本网站业务要求强一致的逻辑很少。

1-异步任务执行

异步任何队列使用Redis-List + Worker池化(生产者消费者模式),任务完成时,如果需要通知用户则发送站内通知或者邮件;将任务进行统一的包装,然后将任务序列化到List中(使用fastjson序列化实例数据),然后消费者将list中的元素取出来反序列化拿到实例数据,然后根据任务的类型调用对应的handler来处理。

2-可以做异步任务队列的操作有

这些对一致性要求不高可以异步来做:注册邮箱验证通知、登录发现IP异常通知、系统批量通知、关注的领域有新动态的通知、被赞、被收藏、有新的回复、朋友私信等。

3-对于互动通知

对于互动中主动发起动作的一方,在前端页面完成操作时,后端直接命中数据库(只读缓存策略下,每一次写操作都是直接命中数据库的,当然也可以使用write behind cached模式,但是要保证数据安全落地),返回操作的结果给发起的一方,然后异步地通知被动的一方。

3- 点赞和收藏功能实现

复杂数据结构的实现(更新操作多,且数据在数据库中不集中,不适合在处理业务时直接使用数据库表,而是利用Redis丰富的数据结构来完成)

1- Write behind Cached缓存更新模式

然后异步定时更新缓存数据到数据库中做持久化,比如为每一个key绑定一个标志作为modifyCount,然后开辟一个数据同步线程,每隔30秒去轮询每一个key的modifyCount,看是否和之前的一样,如果不一样则进行同步,一样则跳过,然后休眠)

2-业务描述:

对于一个分享的内容或者评论,多个用户可以赞或者取消赞,不可重复赞,这个业务要求在展示分享信息时能快速的显示被赞的数量,并把赞的用户头像并排显示,这就需要知道是那些用户对这个信息进行赞了,然后还会对不同信息进行赞数量按不同领域的排名或者总排名,用于在首页推送热门内容。点赞和收藏逻辑一样。

3. 设计解决方案:使用Redis bitmap

1-点赞/取消赞:

比如id为2017的用户对id为1000的内容进行了点赞,则根据内容id生成一个Redis Key,比如key=share_content:1000,然后让其value的第2017偏移位置上设置为1(Jedis.setbit(key,2017,1) O(1)操作)。

2-显示:

当用户打开图片时需要查询当前是否点赞过该内容,并进行前端显示,如果点过赞则将赞标志标注,然后js使点赞按钮失效,只能取消
Jedis.getbit(key,2017) O(1)操作

3-查询

查询某条内容点赞的总次数Jedis.bitcount(key),虽然时间复杂度为O(N)但是其实效率很高

4- 获取信息点赞排名

根据排名的不同领域生成key,比如key=agriculture(农业),则直接将所有信息的content_id和bitcount(作为score)放入sorted set中,Jedis.zadd("agriculture",count,content_id)获取指定内容的排名Jedis.zrank(key,content_id)
获取score排名范围的集合,如获取点赞排名1-100名的内容放在首页展示
Jedis.zrevrange(key,0,99),注意降序,点赞数排名第一的放在0索引上

5- 拓展

可以使用Redis bitop操作来判断某一用户user_id是否对两个内容content_id都点了赞
Jedis.bitop("AND","content_id:1&2","content_id:1","content_id:2"),这条命令将content_id:1和content_id:2的与运算的结果保存到content_id:1&2中
然后查询这content_id:1&2中偏移为user_id的位置来判断 Jedis.getbit("content_id:1&2",user_id)

4- 消息的最终一致性设计方案

  1. 开始一个业务请求Request

  2. 业务请求处理方法Handler发送消息给消息中间存储Message Store Center,其中消息不仅包含业务相关的信息,还包含一个offset ID。

Redis Key的生成方案:根据处理方法的全限定名称以及整时的时间戳(精确到秒)生成一个Key,当方法被调用时先判断当前时间的整时Key是否存在,如果不存在则创建,如果存在则继续下面的流程,Value的值使用Bitmap操作,每一位表示在key创建和Key失效的这段时间内自增业务ID,发送消息时发送表示调用次数的Bitmap offset ID,默认对应的偏移位置上的值为0。

  1. 消息中间件收到消息,存储消息,并标记当前信息为待投递状态。存储成功返回一个确认标志给Handler。

  2. Handler根据接收确认标志的情况进行处理

  • 失败则放弃业务执行并返回一个未完成标志,由上级调用者进行相关的重试策略或者直接放弃整个业务。
  • 成功则开始处理的执行,如果正常成功完成则将对应的Bitmap offset ID设置为1,并发送反馈消息给消息中间件,当处理因为超时或者异常而没有成功则反馈一个失败标志给消息中间件;当然如果业务因为错误而没有正常完成则就不会发送反馈给消息中间件。
  1. Handler处理完成将结果发送给消息中间件

  2. 消息中间件根据Handler反馈的消息进行投递信息。

  • Handler处理成功则进行投递
  • 如果失败则删除消息的存储。
  1. 消息中间件的补偿方案

因为只对超时反馈的信息才会进行询问,所以正常成功完成的Handler都将会设置 offset ID对应的值为1,没有设置的为异常或者超时而失败的Handler。每一次定时任务去询问消息中超时待投递状态的Handler 对应Key的状态:

  • 如果offset ID对应的位置为1则投递(对应Handler反馈在发送中途丢失成功标志的情况);
  • 如果对应的offset ID位置为0则设置对应的offset ID位置的值为1,并删除消息(要保证设置和删除的原子性)(对应Handler反馈在发送中途丢失失败标志的情况);
  • bitcount计算Bitmap中是否所有位置都为1,如果是则删除这个Key,如果不是则跳过。
  1. 消息去重
  1. 发送端去重:每一发送都使用相同的ID(上述的业务自增ID),解决的是消息中间件存储消息后没有向发送方反馈成功存储的确认消息导致重复问题的。
  2. 消息中间件投递去重:使用分布式事务确保消息接收方成功处理和消息状态更新的原子性,也可以是接收方的处理接口设计成幂等性的(不依赖内部状态写数据,完全由消息的内容决定;读操作必然是幂等性的),主要解决的是消息接收方处理成功后没有更新消息的状态导致消息中间件重复投递造成的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,577评论 18 399
  • 简单心理学(不同人的性格与行为联系) 欧洲流行音乐发展 金融与会计的适宜人群
    Whale__fall阅读 125评论 0 0
  • 想孩子了 流泪了 一边责怪自己没有陪伴在孩子身边 一边安慰自己说,孩子长大了会理解的 这种时候经常会出现,特别是夜...
    不想长大的囡囡阅读 248评论 0 0