需要满足如下条件文章目录
一致性
session一致性
tomcat广播 优点:简单容易实现 缺点 当服务器多的时候,广播占用大量带宽,而且每台服务器保存session占用内存 客户端存储 优点 服务器不需要存储 缺点 每次请求需要携带session,占用带宽 安全问题 session存储客户端,受限于cookie限制 nginx固定分配一台服务器处理 4层代理hash,使用ip作为hash分配服务器 7层代理hash,使用业务属性分配,如order_id,user_id等 优点: 只需要修改nginx 支持水平扩展 缺点: 重启,容易造成session丢失 当服务器水平扩展后,服务器访问时重新分配新的主机处理,造成访问不了session 存储内存 找到一个所有服务器都会访问的地方,如数据库,redis.可把session存储在此 优点: 存储服务端,没安全隐患 水平扩展 只要存储session服务器不重启,web-server重启无所谓 缺点: 增加一次网络调用 修改业务代码 建议:redis,因为sessin作为一个高访问的玩意儿,而且丢失也没事,所以存储redis.
分布式事务一致性
二阶段提交(2PC)
引入一个协调者,统一掌控所有参与者,并指示他们是否操作结果提交还是回滚 分为2个阶段 投票阶段:参与则通知协调者,协调者反馈结果 提交阶段:收到参与者反馈后,协调者再向参与者发出通知,根据反馈情况决定每个参与者到底还是提交还是回滚 例子:老大BOSS说要吃饭,统计下面员工意见,有员工ABC同意去,但D没回复,则BOSS依旧处于接受信息状态,ABC则阻塞.当统计了ABCD员工意见后,如果有一个员工不同意,则通知ABCD不去(回滚),然后ABCD收到后ACK BOSS,这个阶段如果BOSS没收到会一直阻塞 缺点 执行过程中,所有节点处于阻塞,所有节点持有资源锁定状态,当有一个节点出现问题无法回复,则会一直阻塞,就需要更复杂的超时机制处理.
补偿事务
在业务端执行业务逆向操作事务 flag1=执行事务一返回结果 if(flag1){ flag2=执行事务二返回结果 if(flag2){ ...执行事务... }else{ 回滚事务二 } }else{ 回滚事务一 } 缺点 嵌套过多 不同业务需要写不同的补偿事务 不具备通用性 没有考虑补偿事务失败
后置提交优化
一个事务,分别为执行和提交2阶段 执行比提交耗时长 改变事务执行与提交时序,变成事务先执行然后一起提交 执行事务一,提交.执行事务耗时200ms,提交2ms 执行事务二,提交.执行事务耗时200ms,提交2ms 执行事务三,提交.执行事务耗时200ms,提交2ms 当执行事务二~三之间出现异常,数据就不一致,时间范围为202+202ms 通过改变时序后: 执行事务一,执行事务耗时200ms 执行事务二,执行事务耗时200ms 执行事务三,执行事务耗时200ms 提交事务一,提交2ms 提交事务二,提交2ms 提交事务三,提交2ms 后置优化后,在提交事务二~三阶段才会出现不一致情况,时间耗时4ms 虽然不能根本解决数据一致问题,但通过缩小时间概率,降低不一致概率 缺点: 串行执行事务提交后,连接就释放了,但后置提交优化后,所有库的连接,需要等待全部事务执行完才能释放,数据库连接占用时间加长,吞吐降低了
MQ实现,三阶段提交,略
数据库主从一致性
当A修改主数据库时,主数据库在同步读数据库阶段,如果有B读取读数据库,因为主数据库还未同步,所以造成B读取的是旧值 解决方案 半同步复制 执行写操作后,等主从同步执行完,在返回请求,这个时候读数据库读到的时最新的数据 优点: 利用数据库原生功能,简单实现 缺点: 写请求时间延长,降低吞吐 强制读主库 优点:不需要修改 缺点:需要采用缓存优化数据库访问效率 数据库中间件 1:所有读走从库,写走主库 2:记录所有路由到主库的key 3:在主从同步时间内,如果有读请求访问,则读主库 4:主从同步时间过后,读请求访问从库 优点:有现成中间件可以使用 缺点:没发现啊- - 缓存记录写key key缓存redis,设置超时时间(一般主从同步时间预估值) 当读请求,cache hit则说明这个key刚进行写操作,则读主库 cache miss,说明说明key近期没有写操作,则读从库 优点:轻量级实现中间件 缺点:引入cache,与业务绑定
数据库双从一致性
冗余写库保证高可用 当有2个请求分别写入AB主库,这个时候生成的ID都是一样的,A同步B,B同步A,就会同步失败,造成数据不一致 解决方案: 设置不同初始值,以及相同步长 设置id生成策略,参考10种分布式id生成 对外只提供一台写库,另外一台作为影子shadow 存在问题:当A库同步B时出现异常,KeepAlive虚ip漂移,切换到B库,在A库同步成功之前,插入一条数据到B库,造成数据不一致问题. 内网DNS探测 上述存在问题,本质需要同步成功后,在实施虚ip漂移 写一个小脚本轮询探测主库ip连通性,当连接失败,则delay一个X秒延时,等主库B同步成功后,在将B库ip连通
库存扣减一致性
直接设置扣减 缺点:当重试时,导致重复扣减,主要原因是扣减是一个非幂等操作,不能重复执行 直接设置最新库存即可,是一个幂等操作 缺点:当存在多个线程设置的时候,容易造成覆盖 数据库CAS扣减 update store set num=$num_new where id=$id and num=$num_old 缺点:基于CAS的设计,当存在多个线程争抢的时候只有一个会成功执行,为了保证后续逻辑,需要增加重试机制,而因为数据库比较重量,所以可以采用redis优化 redis原子性扣减 redis缓存数据库真实库存,原子扣减.后MQ到数据库 需要同步到数据库,增加一次操作,而且当redis宕机,真实的数据还在redis没有来得及同步数据库,容易造成数据不一致问题 MQ缓存请求 利用MQ,将请求线性维护,后台MQ接收端,执行数据库写请求.
十种分布式id生成
UUID 简单,但id本身看不出特殊意义,且不能递增,且Mysql明确表示id越短越好, 取当前毫秒数 id趋势递增,id整数数据库查询效率高,本地生成即可,不需要远程调用,但因为毫秒1000范围内,所以当并发超过1000,容易重复. 数据库自增id 需要一个单独的mysql实例生成id,查询快,id递增,但单点容易宕机,且抗不出并发场景,可使用keepalived+vip做个影子备用,但因此引发新的问题,2个数据库使用率只有50% = = 数据库多主模式 多主解决了单点宕机风险,但id重复.但是可以设置步长,譬如A数据库步长为2,起始为1.B数据库步长为2,起始为2.但引发了新的问题,扩展集群后,需要修改步长,而且影响到之前的AB数据库起始值. 号段模式(发射器) 从数据库批量获取自增id,加载到内存(如redis),然后更改数据库最新起始值,当内存使用完id后在此获取.这里为了取段的时候出现并发场景,可表多设置一个字段,版本号.CAS修改. redis incr,但需要注意持久化问题.而且redis作为缓存服务器不应该与具体业务耦合,不推荐. 雪花算法 正数位+时间戳+机器id+数据中心+自增值,占用8字节.操作灵活,实现简单,只需要维护机器id就行了 TinyId(滴滴) 基于号段 Uidgenerator(百度) 基于雪花算法, Leaf(美团) 同时支持号段模式与雪花算法,
分布式锁
数据库分布式锁
乐观锁增加版本号 根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。 悲观锁 当想要获得锁时,就向数据库中插入一条记录,记录执行方法名(添加唯一约束),释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。 这种方式存在以下几个问题: 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。 只能是非阻塞锁,插入失败直接就报错了,无法重试。 不可重入,同一线程在没有释放锁之前无法再获得锁。
redis分布式锁
redis实现,借鉴Redlock 获得锁:setnx(id,过期时间) exec 删除锁:del(key) 存在问题: 超时,但线程还在工作 开启一个守护线程监控任务是否完成,没完成添加过期时间 无法释放锁:设置超时时间 无法重入: 本地ThreadLocal计数 redis hash存储可重入次数,lub脚本加锁解锁 其他人删除锁 id为唯一标识,删除锁的时候判断是不是自己加的锁,不是不删除 var value=get lock_name if value== 锁的时候填充的值 释放锁 else 释放锁失败 因为这个步骤不是原子性,所以需要lua脚本执行判断和释放锁 死锁:设置获取锁的优先级策略 存储一对key-value,value为当前时间+过期毫秒数 当cur_data>K-value时,获得锁
官方给出了Redlock算法,大致意思如下: 在分布式版本的算法里我们假设我们有N个Redis Master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法(如果您采用的是Redis Cluster集群此方案可能不适用,因为Redis Cluster是按哈希槽 (hash slot)的方式来分配到不同节点上的,明显存在分布式协调算法)。 我们把N设成5,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁: 1、获取当前时间(单位是毫秒)。 2、轮流用相同的key和随机值(客户端的唯一标识)在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。 3、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。 4、如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。 5、如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。 虽然说RedLock算法可以解决单点Redis分布式锁的高可用问题,但如果集群中有节点发生崩溃重启,还是会出现锁的安全性问题。具体出现问题的场景如下: 假设一共有A, B, C, D, E,5个Redis节点,设想发生了如下的事件序列: 1、客户端1成功锁住了A, B, C,获取锁成功(但D和E没有锁住) 2、节点C崩溃重启了,但客户端1在C上加的锁没有持久化下来,丢失了 3、节点C重启后,客户端2锁住了C, D, E,获取锁成功 这样,客户端1和客户端2同时获得了锁(针对同一资源)。针对这样场景,解决方式也很简单,也就是让Redis崩溃后延迟重启,并且这个延迟时间大于锁的过期时间就好。这样等节点重启后,所有节点上的锁都已经失效了。也不存在以上出现2个客户端获取同一个资源的情况了。 总之用Redis集群实现分布式锁要考虑的特殊情况比较多,尤其是服务器比较多的情况下,需要多测试。
/** * 分布式锁的简单实现代码 * Created by liuyang on 2017/4/20. */ public class DistributedLock { private final JedisPool jedisPool; public DistributedLock(JedisPool jedisPool) { this.jedisPool = jedisPool; } /** * 加锁 * @param lockName 锁的key * @param acquireTimeout 获取超时时间 * @param timeout 锁的超时时间 * @return 锁标识 */ public String lockWithTimeout(String lockName, long acquireTimeout, long timeout) { Jedis conn = null; String retIdentifier = null; try { // 获取连接 conn = jedisPool.getResource(); // 随机生成一个value String identifier = UUID.randomUUID().toString(); // 锁名,即key值 String lockKey = "lock:" + lockName; // 超时时间,上锁后超过此时间则自动释放锁 int lockExpire = (int) (timeout / 1000); // 获取锁的超时时间,超过这个时间则放弃获取锁 long end = System.currentTimeMillis() + acquireTimeout; while (System.currentTimeMillis() < end) { if (conn.setnx(lockKey, identifier) == 1) { conn.expire(lockKey, lockExpire); // 返回value值,用于释放锁时间确认 retIdentifier = identifier; return retIdentifier; } // 返回-1代表key没有设置超时时间,为key设置一个超时时间 if (conn.ttl(lockKey) == -1) { conn.expire(lockKey, lockExpire); } try { Thread.sleep(10); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } catch (JedisException e) { e.printStackTrace(); } finally { if (conn != null) { conn.close(); } } return retIdentifier; } /** * 释放锁 * @param lockName 锁的key * @param identifier 释放锁的标识 * @return */ public boolean releaseLock(String lockName, String identifier) { Jedis conn = null; String lockKey = "lock:" + lockName; boolean retFlag = false; try { conn = jedisPool.getResource(); while (true) { // 监视lock,准备开始事务 conn.watch(lockKey); // 通过前面返回的value值判断是不是该锁,若是该锁,则删除,释放锁 if (identifier.equals(conn.get(lockKey))) { Transaction transaction = conn.multi(); transaction.del(lockKey); List<Object> results = transaction.exec(); if (results == null) { continue; } retFlag = true; } conn.unwatch(); break; } } catch (JedisException e) { e.printStackTrace(); } finally { if (conn != null) { conn.close(); } } return retFlag; } }
zookeeper分布式锁
总结
数据库锁: 优点:直接使用数据库,使用简单。 缺点:分布式系统大多数瓶颈都在数据库,使用数据库锁会增加数据库负担。 缓存锁: 优点:性能高,实现起来较为方便,在允许偶发的锁失效情况,不影响系统正常使用,建议采用缓存锁。 缺点:通过锁超时机制不是十分可靠,当线程获得锁后,处理时间过长导致锁超时,就失效了锁的作用。 zookeeper锁: 优点:不依靠超时时间释放锁;可靠性高;系统要求高可靠性时,建议采用zookeeper锁。 缺点:性能比不上缓存锁,因为要频繁的创建节点删除节点。 性能角度:缓存>zookeeper>数据库 可靠性:zookeeper>缓存>数据库
案例
秒杀
围绕秒杀,就6个字,缓存读,异步写 缓存越贴近上游,下游服务器压力就越小 squid/varnish做缓存代理 lvs+keepalived++nginx CDN加速 页面静态化 静态资源nginx代理 js限制点击数 重复点击,返回同一个页面 对同一uid限制请求数和请求速度,以及黑名单拦截 抛弃请求,服务降级 写 比如商品只有2000个,那么只允许2000请求进来,其余的请求返回已售完.MQ接收端可单个接受MQ,也可以批量拉取MQ请求. 数据库层面,如果订单数量是已知的,可提前生成好2000个订单数据,直接update订单和用户关联就行了,而不需要insert.这样也可以避免并发场景数据不一致. 读 redis缓存热点数据 redis缓存请求数,每次请求一个进来,原子减少请求数,当请求数=0,则抛弃后续所有请求. 数据库,主从复制,读写分离,分库分表 业务层面处理 下单和支付放2个环节
ABA问题
执行CAS时,假设A查询库存为N,在将N修改为M途中,有B线程修改了N为M,C线程修改M为N,这个时候A利用下列SQL执行依旧会成功,因为N没有改变,但是实际上N改变了 update store set num=$num_new where id=$id and num=$num_old 针对这个问题添加一个版本号就行了 update store set num=$num_new where id=$id and version=$version
分库分表
主从复制,解决的是数据库集群数据同步问题 读写分离,解决的是读操作效率以及并发量的问题 而分库分表是在数据库级别,解决单表单库数据过量的问题 分库:不同业务数据表部署在不同数据库集群上,垮库不能join) 分表:一张表拆开分别存储在多个数据库中,cobar,amoeba) 数据迁移(利用hash一致算法) 路由算法 hash取余:命中率低至(N/N+1) 一致性hash算法(一致性hash环):通常使用二叉查找树实现。命中率高至99% 缺点:新加入节点只影响附近最近的节点,其他节点还是负载那么多 添加虚拟层,每个物理服务器节点虚拟为一组缓存服务器,然后根据hash值放置hash环上 命中高达75%,同时解决资源分配不合理问题 地理位置 时间范围 数据范围1-1000 A库,1001-2000 B库 单个库太大 垂直切分:表多而数据多,根据业务切分成不同的库。 水平切分:将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 单个表太大 垂直拆分,字段多,拆分成多个表这些表还在同个数据库中 水平拆分,数据多,按照规则将数据分别存储多个相同结构表, 分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合我们处理现实世界问题的方式。 存在问题 分布式事务 范围查询面临问题 多库结果集合并(group by,order by)
本网页所有视频内容由 imoviebox边看边下-网页视频下载, iurlBox网页地址收藏管理器 下载并得到。
ImovieBox网页视频下载器 下载地址: ImovieBox网页视频下载器-最新版本下载
本文章由: imapbox邮箱云存储,邮箱网盘,ImageBox 图片批量下载器,网页图片批量下载专家,网页图片批量下载器,获取到文章图片,imoviebox网页视频批量下载器,下载视频内容,为您提供.
阅读和此文章类似的: 全球云计算