秒杀系统-高并发的优化
秒杀系统面临着如下问题: (1)无法使用cdn缓存,因为系统逻辑不可能放在cdn中。 (2)后端缓存困难:库存问题,因为运用到了mysql事务操作(设置联合主键)。 (3)一行数据竞争:热点商品,因为多个用户同时对数据库某条数据进行操作。 秒杀系统的优化方案: (1)前端控制:暴露接口,按钮防重复提交。 (2)动静态数据分离:cdn缓存,后端Redis缓存。 (3)事务竞争优化:减少事务锁时间,把客户端逻辑放在mysql服务端,避免网络延迟和GC的影响。 GC(Garbage Collection)垃圾回收机制 使用Redis优化地址暴露接口,在dao包下新建一个cache包,然后创建一个RedisDao.java函数,在spring-dao.xml中配置redisdao构造器。 RedisDao.java: public class RedisDao { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final JedisPool jedisPool; public RedisDao(String ip, int port) { jedisPool = new JedisPool(ip, port); } private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class); public Seckill getSeckill(long seckillId) { try { Jedis jedis = jedisPool.getResource(); try { String key = "seckill:" + seckillId; byte[] bytes = jedis.get(key.getBytes()); if (bytes != null) { Seckill seckill = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); return seckill; } } finally { jedis.close(); } } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public String putSeckill(Seckill seckill) { try { Jedis jedis = jedisPool.getResource(); try { String key = "seckill:" + seckill.getSeckillId(); byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); int timeout = 60 * 60; String result = jedis.setex(key.getBytes(), timeout, bytes); return result; } finally { jedis.close(); } } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } } 针对网络延迟和GC导致的并发问题,使用存储过程,将整个事务放在mysql端完成,秒杀存储过程实现如下。 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_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; $$ DELIMITER ; 最后,在ServiceService.java中建立存储执行秒杀操作的函数,在SeckillDao.java实现该接口,在SeckillDao.xml中使用mybatis调用存储过程,在服务端的SeckillServiceImpl.java完成配置即可。