两次MD5加密:前端
理解:java
第一次是为了防止传输过程当中,用户明文密码被截取获取。mysql
第二次是为了防止数据库被盗后,别人知道了密码的md5值,而后直接伪造请求,传递这个md5值过来登陆成功;二次加密后,由于浏览器传递的值和数据库中存放的值不一致,因此丢失后也不会形成密码丢失问题。redis
生成请求地址一一对应的缓存,能防止瞬间并发高带来的性能问题,但页面的实时性就下降了,由于缓存所展现的内容老是固定的,过时从新渲染后,内容才可能发生变化。算法
--建立秒杀库存表 CREATE TABLE seckill( `seckill_id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品库存id', `name` varchar (120) NOT NULL COMMENT '商品名称', `number` int NOT NULL COMMENT '库存数量', `start_time` timestamp NOT NULL COMMENT '秒杀开启时间', `end_time` timestamp NOT NULL COMMENT '秒杀结束时间', `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立时间', PRIMARY KEY(seckill_id), key idx_start_time(start_time), key idx_end_time(end_time), key idx_create_time(create_time) )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表'; --秒杀成功明细表 CREATE TABLE success_killed( `seckill_id` bigint NOT NULL COMMENT '秒杀商品id', `user_phone` bigint NOT NULL COMMENT '用户手机号,简单起见经过手机来惟一对应用户', `state` bigint NOT NULL DEFAULT -1 COMMENT '状态表示:-1:无效,0:成功,1:已付款', `create_time` timestamp NOT NULL COMMENT '建立时间', PRIMARY KEY (seckill_id,user_phone), key idx_create_time(create_time) )ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';
客户端刷新来访问接口:以服务端的时间和数据库中记录的活动的时间进行对比,来判断秒杀活动是否处于开启中。开启的话,则返回一个惟一令牌。sql
发送秒杀请求时,参数中须要有一个活动id加盐md5计算得出的令牌,这个令牌与当前秒杀活动一一对应,之因此要传令牌,而不是传活动的id,是为了防止别人在活动开始前作好工具来自动秒杀,活动id能提早拿获得,而令牌是活动开始后才能拿获得。这样一来就算作好了秒杀工具,也要在活动开始以后来改参数,工具才能用。可是改参数的话,速度就比别人慢了,别人进入页面,点一个按钮就抢完了。这就从必定程度上防范了自动化秒杀工具的使用。数据库
执行秒杀时,回传秒杀地址中的md5令牌,服务端以相同的算法计算出实际令牌,校验令牌准确性,准确才能够执行后续操做。后端
理解:令牌仅仅在活动开始才会给浏览器生成,而且秒杀操做必需要有令牌,就能保证秒杀的公平性。浏览器
以上令牌实际有两种生成方式:缓存
1. md5(productId + 固定的盐) => 令牌:每一个用户的令牌都相同
2. md5(productId + userId) => 令牌 :每一个用户的令牌都不一样
不一样人输入验证码的速度不同,因此能够下降(分散)一个短暂时间内的并发请求的压力,同时还能防止机器人刷请求。
数学公式验证码,如1+2-3,输入运算的结果
生成图片背景:随机的弧线、线段,用于干扰
生成公式:如生成三个随机数,这三个随机数之间插入两个随机的运算符,四则运算分别对应0-3这四个随机数(生成这个公式注意/0的状况)。
公式以字符串形式存在。使用ScriptEngineManager这个类对字符串公式进行运算,得出结果。而后结果对应用户,保存到redis中,后续秒杀请求中,要对提交上来的验证结果进行校验。校验经过以后,将redis中的结果移除掉,由于校验经过一次后续就没有必要再保留了
限制同一个用户一段时间(10秒内或一分钟等)内对接口的访问次数。
每次访问接口,次数记录到redis中,key=接口名+userid。使用incr进行增长,设置这个key的有效期,超过以后自动删除。递增以后,若是大于某个值,则接口返回拒绝访问。
实现思路:拦截器中计算访问次数,没有超过次数,则放行请求。
第一次设置的时候,就设置有效期为1s:
注解实现配置化,同时设置好后续要用的用户信息
建立一个拦截器,获取方法上的注解,并进行计数判断,肯定是否放行(光标处未给出)
在拦截器中获取好user对象,设置到threadlocal中,接着service方法中就能获取到了。
或者使用【注入自定义对象】,在处理器的处理方法中从threadLocal中获取对象,这样service方法直接注入user对象就可使用了
某一个商品进入秒杀时,成为热点数据,同一个数据行的竞争变得很是大。
服务启动后,监听bean生命周期,进行redis数据冗余(productId->productCount)。
接收秒杀请求后,执行redis decr命令,返回数据自减以后的值,若是这个值大于0,说明秒杀成功,由于没有操做mysql,实际的操做不多,一会儿就响应给客户端了,提示客户端正在排队中,服务后台往MQ中放一个消息,表示某个用户秒杀到了某个商品。
客户端轮训服务器,以更新当前当前的排队中的状态,以得知是否成功,服务接口查询mysql中的秒杀记录,而后将当前的状态返回给客户端。
服务器不停地消费消息,实现数据落地(mysql中生成订单、库存数量减1、生成秒杀记录)。这里的逻辑相对于秒杀请求来讲,是异步执行的,生成秒杀记录用于给客户端轮训时更新客户端的状态。数据落地过程当中,可能还没开始执行,则当前秒杀记录为空,执行成功、执行失败时,都要记录秒杀记录,标记成功仍是失败;失败的状况有:库存数量减一时,发现没得减了,则失败。但这个状况基本不会出现,由于已经提早冗余了一份数据到redis中,redis能成功减一的话,mysql里也能成功减一的,由于这两份数据在最开始时,数值是同样的。
以上的细节优化:
redis预减库存时,调用decr函数,这其实是一个阻塞的网络请求。这个函数返回的是减了以后的结果,因此只要有一次出现负数,后续的全部调用都会是负数,这样的网络请求没有必要。因此在服务器内存中,创建一个map(productId -> boolean),标记某个商品是否出现负数了,作好标记后,后续再出现redis预减状况,先判断这个map,而不用每次都调用decr函数了,少一次网络请求,提高接口的响应速度。
对MQ的理解:
不用MQ以前,触发某些逻辑是直接调用对应的函数的,同步的话,则会形成阻塞。
使用MQ后,触发某些逻辑就不是调用函数了,而是往消息队列中发送一个消息,而后当前函数就结束了;
另外一个地方提早注册好了消息的处理函数,这个函数会被异步执行,也就是这时候才实际触发了某些逻辑。经过消息队列来解除逻辑之间的同步关系,实现异步调用。
经过MQ实现了异步调用。这提高了QPS,可是感受有点奇怪,是否是能够认为这个QPS意义不大,就好比一我的来找你办事情,你还没办就直接跟他说我登记一下,你先回去等通知吧,一小时来了一百我的,你都这么处理,而后跟别说,我这一小时内处理了一百我的的事情,说这句话看上去效率很是高,但实际上有变高吗?由于实际要作的事情时延迟作了。不使用异步处理的话,对于一小时的秒杀活动,则全部的数据处理必须在这一小时内处理完,这会给服务器带来大负荷。而使用异步处理后,这一小时内只须要执行redis预减就好了,而不用执行数据落地,这一小时内执行的是秒杀活动最关键的部分,就是谁能抢获得,就只作这个工做,其余有空在作,如实际的数据落地和状态更新能够在这一小时以后才执行,至关于把这一小时内的集中的工做量,平摊到后续的一段时间内了。实际能够理解为:一件事情的处理效率没有本质变化,只不过异步的话,可让完成这件事情有更多的时间,因此完成起来就更加游刃有余了。
缺点:分布式redis、分布式MQ的运维成本、保证数据一致性、如何数据回滚、又须要一个分布式系统来记录谁已经秒杀过了,而不能再屡次秒杀、不适合新手
mysql测试,同一行的高并发update,能达到4W QPS左右,因此纯粹讨论mysql,也不慢了。
常规实现的瓶颈分析,一次秒杀过程须要发送4次数据库指令:
开启事务、update减库存、(期间还可能会存在GC耗时)、insert添加购买明细、提交事务【数据库服务器与java客户端之间也存在网络延时】
占用行级锁的时间 = update语句开始占用的时间(而不是事务开始时间) 至 提交事务/回滚事务,释放行级锁时间
以上这个时间内包含了 GC耗时、update语句结果返回耗时,insert语句和结果往返耗时。简单来讲就是行级锁占用时间=GC耗时 + java服务端与mysql服务端的网络通讯耗时
而对同一行数据的秒杀操做,并发量与行级锁占用时间直接相关。以上步骤出现并发执行时,只能串行。由于每一个事务锁住的都是同一行数据。
优化方式:把Java客户端逻辑放到mysql服务端,避免网络延迟和GC耗时
1.mysql源码层的修改方案,定制update语句,使update语句更新行为1时,自动提交,小公司基本没有这样的团队实力去作这样的事
2.使用存储过程,存储过程的目的是将一组sql组成一个事务,在数据库服务端完成,避免客户端去完成事务,从而下降性能的消耗(避免了java GC耗时和mysql与java服务器之间的通讯耗时)
以上占用行级锁的时间中,包含了insert操做,因此把insert操做移出来,能提高下降行级锁占用时间。
把insert和update的位置互换一下。
被insert的表使用联合主键(用户手机、秒杀商品id),若是成功插入数据,说明用户之前没秒杀过,插入失败则说明是重复秒杀了。插入成功返回1,失败返回0(这个实现经过insert ignore来实现:加了ignore,若是出现主键冲突,则不报错,而是返回0);
仅当插入成功后,才进行update减库存。若是update影响行数是1,则说明能秒杀到,不然影响行数为0,说明没秒杀到。没秒杀到,则抛出异常,好让事务回滚。
以上优化延迟获取行级锁,而且行级锁的获取到释放之间(事务回滚或提交)的操做更少了,则占用行级锁的时间更少了。原来的逻辑,期间有一个insert操做,互换位置以后,insert操做就不占用行级锁时间了。
理解:行级锁占用从update开始至事务结束,期间的操做越少越好,因此把insert移出去,行锁占用时间,并发效率更高。
// 先使用insert ignore插入秒杀记录,返回影响行数 int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); if (insertCount <= 0) { //出现重复秒杀 throw new RepeatKillException("seckill repeated"); } else { // 库存减一,这里开始占用行锁: update table set num = num - 1 where num > 0 int updateCount = seckillDao.reduceNumber(seckillId, nowTime); if (updateCount <= 0) { //没有更新到记录,跑出异常,使事务回滚,还原以前的插入操做 throw new SeckillCloseException("seckill is closed"); } else { SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); // 返回秒杀成功 return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled); } }
使用存储过程,避免GC耗时,以及最大程度下降了java服务端与mysql服务器之间的网络通讯耗时。由于使用存储过程,就只有一次请求往返耗时。
insert ignore:若是中已经存在相同的记录(惟一索引、主键来判断),则忽略当前新数据。【success_killed表使用id和phone做为联合主键】
-- 秒杀执行存储过程 DELIMITER $$ -- console ; 转换为 $$ -- 定义存储过程 -- 参数: in 输入参数; out 输出参数 -- row_count():返回上一条修改类型sql(delete,insert,update)的影响行数 -- row_count: 0:未修改数据; >0:表示修改的行数; <0:sql错误/未执行修改sql CREATE PROCEDURE `seckill`.`execute_seckill` (in v_seckill_id bigint,in v_phone bigint, in v_kill_time timestamp,out r_result int) BEGIN DECLARE insert_count int DEFAULT 0; START TRANSACTION; insert ignore into success_killed (seckill_id,user_phone,create_time) values (v_seckill_id,v_phone,v_kill_time); select row_count() into insert_count; IF (insert_count = 0) THEN ROLLBACK; set r_result = -1; ELSEIF(insert_count < 0) THEN ROLLBACK; SET R_RESULT = -2; ELSE update seckill set number = number-1 where seckill_id = v_seckill_id and end_time > v_kill_time and start_time < v_kill_time and number > 0; select row_count() into insert_count; IF (insert_count = 0) THEN ROLLBACK; set r_result = 0; ELSEIF (insert_count < 0) THEN ROLLBACK; set r_result = -2; ELSE COMMIT; set r_result = 1; END IF; END IF; END; $$ -- 存储过程定义结束
前端控制,点击秒杀以后,隐藏按钮,防止再次点击
动静态数据分离,CDN缓存(将请求从咱们的服务中剥离出去),后端缓存(redis)
事务竞争优化,减小事务锁的时间
redis缓存:实现对热点数据的快速存取,分摊mysql的压力
java对象存入redis:序列化为 byte[] 存入,取出byte[] ,反序列为实际对象
序列化最高效的是protostuff。这是谷歌protobuf的再次包装,而不须要本身再写描述文件来帮助序列化和反序列化了,包装后能动态生成描述文件
从redis获取数据,并反序列化
序列化对象,并存入redis
作好的java服务,不直接暴露出去,而是经过ngnix反向代理
完整附件:https://files.cnblogs.com/files/hellohello/seckill-master.7z