---------------------------------------------------------------------------------------------
[版权申明:本文系作者原创,转载请注明出处]
文章出处:http://blog.csdn.net/sdksdk0/article/details/52997034
作者:朱培 ID:sdksdk0
--------------------------------------------------------------------------------------------
好久没写博客了,因为一直在忙项目和其他工作中的事情,最近有空,刚好看到了一个秒杀系统的设计,感觉还是非常不错的一个系统,于是在这里分享一下。
秒杀场景主要两个点:
1:流控系统,防止后端过载或不必要流量进入,因为慕课要求课程的长度和简单性,没有加。
2:减库存竞争,减库存的update必然涉及exclusive lock ,持有锁的时间越短,并发性越高。
对于抢购系统来说,首先要有可抢购的活动,而且这些活动具有促销性质,比如直降500元。其次要求可抢购的活动类目丰富,用户才有充分的选择性。马上就双十一了,用户剁手期间增量促销活动量非常多,可能某个活动力度特别大,大多用户都在抢,必然对系统是一个考验。这样抢购系统具有秒杀特性,并发访问量高,同时用户也可选购多个限时抢商品,与普通商品一起进购物车结算。这种大型活动的负载可能是平时的几十倍,所以通过增加硬件、优化瓶颈代码等手段是很难达到目标的,所以抢购系统得专门设计。
在这里以秒杀单个功能点为例,以ssm框架+mysql+redis等技术来说明。
一、数据库设计
使用mysql数据库:这里主要是两个表,主要是一个商品表和一个购买明细表,在这里用户的购买信息的登录注册这里就不做了,用户购买时需要使用手机号码来进行秒杀操作,购买成功使用的是商品表id和购买明细的用户手机号码做为双主键。
CREATE DATABASE seckill; USE seckill; 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 '秒杀库存表' --初始化数据 INSERT INTO seckill(NAME,number,start_time,end_time) VALUES ('4000元秒杀ipone7',300,'2016-11-5 00:00:00','2016-11-6 00:00:00'), ('3000元秒杀ipone6',200,'2016-11-5 00:00:00','2016-11-6 00:00:00'), ('2000元秒杀ipone5',100,'2016-11-5 00:00:00','2016-11-6 00:00:00'), ('1000元秒杀小米5',100,'2016-11-5 00:00:00','2016-11-6 00:00:00'); --秒杀成功明细表 --用户登录认证相关的信息 CREATE TABLE success_kill( seckill_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '秒杀商品id', user_phone BIGINT NOT NULL COMMENT '用户手机号', state TINYINT 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 AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT '秒杀成功明细表' SELECT * FROM seckill; SELECT * FROM success_kill;
这里我们还用到了一个存储过程,所以我们新建一个存储过程来处理,对于近日产品公司来说存储过程的使用还是比较多的,所以存储过程也还是要会写的。
DELIMITER $$ 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_kill(seckill_id,user_phone,create_time,state) VALUE(v_seckill_id,v_phone,v_kill_time,0); 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; $$ DELIMITER ; SET @r_result = -3; CALL execute_seckill(1000,13813813822,NOW(),@r_result); SELECT @r_result;
先可以看一下页面的展示情况:
二、实体类
因为我们有两个表,所以自然建两个实体bean啦!新建一个Seckill.java
private long seckillId; private String name; private int number; private Date startTime; private Date endTime; private Date createTime;
实现其getter/setter方法。
再新建一个SuccessKill。
private long seckillId; private long userPhone; private short state; private Date createTime; private Seckill seckill;
实现其getter/setter方法。
三、DAO接口层
接口我们也是来两个:SeckillDao.java和SuccessKillDao.java
内容分别为:
public interface SeckillDao { //减库存 int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime); Seckill queryById(long seckilled); List<Seckill> queryAll(@Param("offset") int offset,@Param("limit") int limit); public void seckillByProcedure(Map<String, Object> paramMap); }
public interface SuccessKillDao { /** * 插入购买明细 * * @param seckillId * @param userPhone * @return */ int insertSuccessKill(@Param("seckillId")long seckillId,@Param("userPhone")long userPhone); /** * 根据id查询 * * @param seckill * @return */ SuccessKill queryByIdWithSeckill(@Param("seckillId")long seckillId,@Param("userPhone")long userPhone); }
四、mapper处理
在mybatis中对上面的接口进行实现,这里可以通过mybatis来实现。
<mapper namespace="cn.tf.seckill.dao.SeckillDao"> <update id="reduceNumber" > update seckill set number=number-1 where seckill_id=#{seckillId} and start_time <![CDATA[<=]]>#{killTime} and end_time>=#{killTime} and number >0 </update> <select id="queryById" resultType="Seckill" parameterType="long"> select seckill_id,name,number,start_time,end_time,create_time from seckill where seckill_id =#{seckillId} </select> <select id="queryAll" resultType="Seckill"> select seckill_id,name,number,start_time,end_time,create_time from seckill order by create_time desc limit #{offset},#{limit} </select> <select id="seckillByProcedure" statementType="CALLABLE"> call execute_seckill( #{seckillId,jdbcType=BIGINT,mode=IN}, #{phone,jdbcType=BIGINT,mode=IN}, #{killTime,jdbcType=TIMESTAMP,mode=IN}, #{result,jdbcType=INTEGER,mode=OUT} ) </select> </mapper>
<mapper namespace="cn.tf.seckill.dao.SuccessKillDao"> <insert id="insertSuccessKill"> insert ignore into success_kill(seckill_id,user_phone,state) values (#{seckillId},#{userPhone},0) </insert> <select id="queryByIdWithSeckill" resultType="SuccessKill"> select sk.seckill_id, sk.user_phone, sk.create_time, sk.state, s.seckill_id "seckill.seckill_id", s.name "seckill.name", s.number "seckill.number", s.start_time "seckill.start_time", s.end_time "seckill.end_time", s.create_time "seckill.create_time" from success_kill sk inner join seckill s on sk.seckill_id=s.seckill_id where sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone} </select> </mapper>
五、redis缓存处理
在这里我们说的库存不是真正意义上的库存,其实是该促销可以抢购的数量,真正的库存在基础库存服务。用户点击『提交订单』按钮后,在抢购系统中获取了资格后才去基础库存服务中扣减真正的库存;而抢购系统控制的就是资格/剩余数。传统方案利用数据库行锁,但是在促销高峰数据库压力过大导致服务不可用,目前采用redis集群(16分片)缓存促销信息,例如促销id、促销剩余数、抢次数等,抢的过程中按照促销id散列到对应分片,实时扣减剩余数。当剩余数为0或促销删除,价格恢复原价。
这里使用的是redis来进行处理。这里使用的是序列化工具RuntimeSchema。
在pom.xml中配置如下:
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.2</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-api</artifactId> <version>1.0.8</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.0.8</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.0.8</version> </dependency>
然后我们引入之后,直接在这个dao中进行处理即可,就是把数据从redis中读取出来以及把数据存到redis中,如果redis中有这个数据就直接读,如果没有就存进去。
public class RedisDao { private Logger logger = LoggerFactory.getLogger(this.getClass()); private JedisPool jedisPool; private int port; private String ip; public RedisDao(String ip, int port) { this.port = port; this.ip = ip; } //Serialize function private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class); public Seckill getSeckill(long seckillId) { jedisPool = new JedisPool(ip, port); //redis operate try { Jedis jedis = jedisPool.getResource(); try { String key = "seckill:" + seckillId; //由于redis内部没有实现序列化方法,而且jdk自带的implaments Serializable比较慢,会影响并发,因此需要使用第三方序列化方法. byte[] bytes = jedis.get(key.getBytes()); if(null != bytes){ Seckill seckill = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes,seckill,schema); //reSerialize return seckill; } } finally { jedisPool.close(); } } catch (Exception e) { logger.error(e.getMessage(),e); } return null; } public String putSeckill(Seckill seckill) { jedisPool = new JedisPool(ip, port); //set Object(seckill) ->Serialize -> byte[] try{ Jedis jedis = jedisPool.getResource(); try{ String key = "seckill:"+seckill.getSeckillId(); byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); //time out cache int timeout = 60*60; String result = jedis.setex(key.getBytes(),timeout,bytes); return result; }finally { jedisPool.close(); } }catch (Exception e){ logger.error(e.getMessage(),e); } return null; } }
还需要在spring中进行配置:我这里的地址使用的是我服务器的地址。
<bean id="redisDao" class="cn.tf.seckill.dao.cache.RedisDao"> <constructor-arg index="0" value="115.28.16.234"></constructor-arg> <constructor-arg index="1" value="6379"></constructor-arg> </bean>
六、service接口及其实现
接下来就是service的处理了。这里主要是由两个重要的业务接口。
1、暴露秒杀 和 执行秒杀 是两个不同业务,互不影响 2、暴露秒杀 的逻辑可能会有更多变化,现在是时间上达到要求才能暴露,说不定下次加个别的条件才能暴露,基于业务耦合度考虑,分开比较好。3、重新更改暴露秒杀接口业务时,不会去影响执行秒杀接口,对于测试都是有好处的。。。另外 不好的地方是前端需要调用两个接口才能执行秒杀。
//从使用者角度设计接口,方法定义粒度,参数,返回类型 public interface SeckillService { List<Seckill> getSeckillList(); Seckill getById(long seckillId); //输出秒杀开启接口地址 Exposer exportSeckillUrl(long seckillId); /** * 执行描述操作 * * @param seckillId * @param userPhone * @param md5 */ SeckillExecution executeSeckill(long seckillId,long userPhone,String md5) throws SeckillCloseException,RepeatKillException,SeckillException; /** * 通过存储过程执行秒杀 * @param seckillId * @param userPhone * @param md5 */ SeckillExecution executeSeckillByProcedure(long seckillId, long userPhone, String md5); }
实现的过程就比较复杂了,这里加入了前面所说的存储过程还有redis缓存。这里做了一些异常的处理,以及数据字典的处理。
@Service public class SeckillServiceImpl implements SeckillService{ private Logger logger=LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillDao seckillDao; @Autowired private SuccessKillDao successKillDao; @Autowired private RedisDao redisDao; //加盐处理 private final String slat="xvzbnxsd^&&*)(*()kfmv4165323DGHSBJ"; public List<Seckill> getSeckillList() { return seckillDao.queryAll(0, 4); } public Seckill getById(long seckillId) { return seckillDao.queryById(seckillId); } public Exposer exportSeckillUrl(long seckillId) { //优化点:缓存优化 Seckill seckill = redisDao.getSeckill(seckillId); if (seckill == null) { //访问数据库 seckill = seckillDao.queryById(seckillId); if (seckill == null) { return new Exposer(false, seckillId); } else { //放入redis redisDao.putSeckill(seckill); } } Date startTime = seckill.getStartTime(); Date endTime = seckill.getEndTime(); //当前系统时间 Date nowTime = new Date(); if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) { return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); } //转换特定字符串的过程,不可逆 String md5 = getMD5(seckillId); return new Exposer(true, md5, seckillId); } @Transactional public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException { if (md5 == null || (!md5.equals(getMD5(seckillId)))) { throw new SeckillException("Seckill data rewrite"); } //执行秒杀逻辑:减库存,记录购买行为 Date nowTime = new Date(); try { //记录购买行为 int insertCount = successKillDao.insertSuccessKill(seckillId, userPhone); if (insertCount <= 0) { //重复秒杀 throw new RepeatKillException("Seckill repeated"); } else { //减库存 int updateCount = seckillDao.reduceNumber(seckillId, nowTime); if (updateCount <= 0) { //没有更新到记录,秒杀结束 throw new SeckillCloseException("Seckill is closed"); } else { //秒杀成功 SuccessKill successKilled = successKillDao.queryByIdWithSeckill(seckillId, userPhone); return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); } } } catch (SeckillCloseException e1) { throw e1; } catch (RepeatKillException e2) { throw e2; } catch (Exception e) { logger.error(e.getMessage()); //所有编译期异常转换为运行时异常 throw new SeckillException("Seckill inner error" + e.getMessage()); } } /** * @param seckillId * @param userPhone * @param md5 * @return * @throws SeckillException * @throws RepeteKillException * @throws SeckillCloseException */ public SeckillExecution executeSeckillByProcedure(long seckillId, long userPhone, String md5) { if (md5 == null || (!md5.equals(getMD5(seckillId)))) { throw new SeckillException("Seckill data rewrite"); } Date killTime = new Date(); Map<String, Object> map = new HashMap<String, Object>(); map.put("seckillId", seckillId); map.put("phone", userPhone); map.put("killTime", killTime); map.put("result", null); //执行存储过程,result被赋值 try { seckillDao.seckillByProcedure(map); //获取result int result = MapUtils.getInteger(map, "result", -2); if (result == 1) { SuccessKill successKilled = successKillDao.queryByIdWithSeckill(seckillId, userPhone); return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); } else { return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result)); } } catch (Exception e) { logger.error(e.getMessage(), e); return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); } } private String getMD5(long seckillId) { String base = seckillId + "/" + slat; String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } }
七、Controller层处理
在springMVC中,是基于restful风格来对访问地址进行处理,所以我们在控制层也这样进行处理。
@Controller @RequestMapping("/seckill") public class SeckillController { private final Logger logger=LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @RequestMapping(value="/list",method=RequestMethod.GET) public String list(Model model){ List<Seckill> list = seckillService.getSeckillList(); model.addAttribute("list",list); return "list"; } @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model){ if(seckillId == null){ return "redirect:/seckill/list"; } Seckill seckill = seckillService.getById(seckillId); if(seckill == null){ return "redirect:/seckill/list"; } model.addAttribute("seckill", seckill); return "detail"; } @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId){ SeckillResult<Exposer> result; try { Exposer exposer = seckillService.exportSeckillUrl(seckillId); result = new SeckillResult<Exposer>(true,exposer); } catch (Exception e) { result = new SeckillResult<Exposer>(false, e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"}) @ResponseBody public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId")Long seckillId, @PathVariable("md5")String md5, @CookieValue(value = "killPhone", required = false)Long phone){ if(phone == null){ return new SeckillResult<>(false, "未注册"); } try { SeckillExecution execution = seckillService.executeSeckillByProcedure(seckillId, phone, md5); return new SeckillResult<SeckillExecution>(true, execution); } catch (SeckillCloseException e) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL); return new SeckillResult<SeckillExecution>(false, execution); } catch (RepeatKillException e) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END); return new SeckillResult<SeckillExecution>(false, execution); } catch (Exception e) { logger.error(e.getMessage(), e); SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); return new SeckillResult<SeckillExecution>(false, execution); } } @RequestMapping(value = "/time/now", method = RequestMethod.GET) @ResponseBody public SeckillResult<Long> time(){ Date now = new Date(); return new SeckillResult<>(true, now.getTime()); } }
八、前台处理
后台数据处理完之后就是前台了,对于页面什么的就直接使用bootstrap来处理了,直接调用bootstrap的cdn链接地址。
页面的代码我就不贴出来了,可以到源码中进行查看,都是非常经典的几个页面。值得一提的是这个js的分模块处理。
//存放主要交互逻辑的js代码 // javascript 模块化(package.类.方法) var seckill = { //封装秒杀相关ajax的url URL: { now: function () { return '/SecKill/seckill/time/now'; }, exposer: function (seckillId) { return '/SecKill/seckill/' + seckillId + '/exposer'; }, execution: function (seckillId, md5) { return '/SecKill/seckill/' + seckillId + '/' + md5 + '/execution'; } }, //验证手机号 validatePhone: function (phone) { if (phone && phone.length == 11 && !isNaN(phone)) { return true;//直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true } else { return false; } }, //详情页秒杀逻辑 detail: { //详情页初始化 init: function (params) { //手机验证和登录,计时交互 //规划我们的交互流程 //在cookie中查找手机号 var killPhone = $.cookie('killPhone'); //验证手机号 if (!seckill.validatePhone(killPhone)) { //绑定手机 控制输出 var killPhoneModal = $('#killPhoneModal'); killPhoneModal.modal({ show: true,//显示弹出层 backdrop: 'static',//禁止位置关闭 keyboard: false//关闭键盘事件 }); $('#killPhoneBtn').click(function () { var inputPhone = $('#killPhoneKey').val(); console.log("inputPhone: " + inputPhone); if (seckill.validatePhone(inputPhone)) { //电话写入cookie(7天过期) $.cookie('killPhone', inputPhone, {expires: 7, path: '/SecKill'}); //验证通过 刷新页面 window.location.reload(); } else { //todo 错误文案信息抽取到前端字典里 $('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300); } }); } //已经登录 //计时交互 var startTime = params['startTime']; var endTime = params['endTime']; var seckillId = params['seckillId']; $.get(seckill.URL.now(), {}, function (result) { if (result && result['success']) { var nowTime = result['data']; //解决计时误差 var userNowTime = new Date().getTime(); console.log('nowTime:' + nowTime); console.log('userNowTime:' + userNowTime); //计算用户时间和系统时间的差,忽略中间网络传输的时间(本机测试大约为50-150毫秒) var deviationTime = userNowTime - nowTime; console.log('deviationTime:' + deviationTime); //考虑到用户时间可能和服务器时间不一致,开始秒杀时间需要加上时间差 startTime = startTime + deviationTime; // //时间判断 计时交互 seckill.countDown(seckillId, nowTime, startTime, endTime); } else { console.log('result: ' + result); alert('result: ' + result); } }); } }, handlerSeckill: function (seckillId, node) { //获取秒杀地址,控制显示器,执行秒杀 node.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>'); $.post(seckill.URL.exposer(seckillId), {}, function (result) { //在回调函数种执行交互流程 if (result && result['success']) { var exposer = result['data']; if (exposer['exposed']) { //开启秒杀 //获取秒杀地址 var md5 = exposer['md5']; var killUrl = seckill.URL.execution(seckillId, md5); console.log("killUrl: " + killUrl); //绑定一次点击事件 $('#killBtn').one('click', function () { //执行秒杀请求 //1.先禁用按钮 $(this).addClass('disabled');//,<-$(this)===('#killBtn')-> //2.发送秒杀请求执行秒杀 $.post(killUrl, {}, function (result) { if (result && result['success']) { var killResult = result['data']; var state = killResult['state']; var stateInfo = killResult['stateInfo']; //显示秒杀结果 node.html('<span class="label label-success">' + stateInfo + '</span>'); } }); }); node.show(); } else { //未开启秒杀(由于浏览器计时偏差,以为时间到了,结果时间并没到,需要重新计时) var now = exposer['now']; var start = exposer['start']; var end = exposer['end']; var userNowTime = new Date().getTime(); var deviationTime = userNowTime - nowTime; start = start + deviationTime; seckill.countDown(seckillId, now, start, end); } } else { console.log('result: ' + result); } }); }, countDown: function (seckillId, nowTime, startTime, endTime) { console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime); var seckillBox = $('#seckill-box'); if (nowTime > endTime) { //秒杀结束 seckillBox.html('秒杀结束!'); } else if (nowTime < startTime) { //秒杀未开始,计时事件绑定 var killTime = new Date(startTime);//todo 防止时间偏移 seckillBox.countdown(killTime, function (event) { //时间格式 var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 '); seckillBox.html(format); }).on('finish.countdown', function () { //时间完成后回调事件 //获取秒杀地址,控制现实逻辑,执行秒杀 console.log('______fininsh.countdown'); seckill.handlerSeckill(seckillId, seckillBox); }); } else { //秒杀开始 seckill.handlerSeckill(seckillId, seckillBox); } } }
用户秒杀之前需要先登记用户的手机号码,这个号码会保存在cookie中。
到了秒杀开始时间段,用户就可以点击按钮进行秒杀操作。
每个用户只能秒杀一次,不能重复秒杀,如果重复执行,会显示重复秒杀。
秒杀倒计时:
总结:其实在真实的秒杀系统中,我们是不直接对数据库进行操作的,我们一般是会放到redis中进行处理,企业的秒杀目前应该考虑使用redis,而不是mysql。其实高并发是个伪命题,根据业务场景,数据规模,架构的变化而变化。开发高并发相关系统的基础知识大概有:多线程,操作系统IO模型,分布式存储,负载均衡和熔断机制,消息服务,甚至还包括硬件知识。每块知识都需要一定的学习周期,需要几年的时间总结和提炼。
源码地址: https://github.com/sdksdk0/SecKill