解决思路:
解决秒杀接口
对于秒杀接口而言,需要使用到Redis将数据进行缓存起来。那么用户就访问就不用去访问数据库了,我们给Redis缓存的数据就好了。
这次使用Jedis来操作Redis.
还有值得 注意的地方:我们可以使用ProtostuffIOUtil来代替JDK的序列化,因为这个的序列化功能比JDK的要做得好很多!
package com.suny.dao.cache; import com.dyuproject.protostuff.LinkedBuffer; import com.dyuproject.protostuff.ProtostuffIOUtil; import com.dyuproject.protostuff.runtime.RuntimeSchema; import com.suny.entity.Seckill; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; /** * 操作Redis的dao类 * Created by 孙建荣 on 17-5-27.下午4:44 */ public class RedisDao { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final JedisPool jedisPool; private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class); public RedisDao(String ip, int port) { jedisPool = new JedisPool(ip, port); } public Seckill getSeckill(long seckillId) { // redis操作业务逻辑 try (Jedis jedis = jedisPool.getResource()) { String key = "seckill:" + seckillId; // 并没有实现内部序列化操作 //get->byte[]字节数组->反序列化>Object(Seckill) // 采用自定义的方式序列化 // 缓存获取到 byte[] bytes = jedis.get(key.getBytes()); if (bytes != null) { // 空对象 Seckill seckill = schema.newMessage(); ProtostuffIOUtil.mergeFrom(bytes, seckill, schema); // seckill被反序列化 return seckill; } } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } public String putSeckill(Seckill seckill) { // set Object(Seckill) -> 序列化 -> byte[] try (Jedis jedis = jedisPool.getResource()) { String key = "seckill:" + seckill.getSeckillId(); byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE)); // 超时缓存 int timeout=60*60; return jedis.setex(key.getBytes(), timeout, bytes); } catch (Exception e) { logger.error(e.getMessage(), e); } return null; } }
<!--导入连接redis的JAR包--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.9.0</version> </dependency> <!--添加序列化依赖--> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-core</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>com.dyuproject.protostuff</groupId> <artifactId>protostuff-runtime</artifactId> <version>1.1.1</version> </dependency>
RedisDao并不受Mybatis的代理影响,于是是需要我们自己手动创建的。
最终,我们的service逻辑就会变成这样子:
秒杀操作优化
再次回到我们的秒杀操作,其实需要优化的地方就是我们的GC和行级锁等待的时间。
我们之前的逻辑是这样的:先执行减库存操作,再插入购买成功的记录
其实,我们可以先插入成功购买的记录,再执行减库存的操作!
- 那这两者有啥区别呢???减库存的操作会导致行级锁的等待,而我们先进行insert的话,那么就不会被行级锁所干扰了。并且,我们这中两个操作是在同一个事物中的,并不会出现“超卖”的情况!
关于先执行insert与先执行update的区别,两个事务同时insert的情况下,没有锁竞争,执行速度会快,当两个事务先update同一行数据,会有一个事务获得行锁,锁在事务提交之前都不会释放,所以让锁被持有的时间最短能提升效率
所以我们service层的逻辑可以改成这样:
这不是最终的方案,如果为了性能的优化我们还可以将SQL在Mysql中运行,不受Spring的事务来管理。在Mysql使用存储过程来进行提交性能
-- 秒杀执行储存过程 DELIMITER $$ -- console ; 转换为 $$ -- 定义储存过程 -- 参数: in 参数 out输出参数 -- row_count() 返回上一条修改类型sql(delete,insert,update)的影响行数 -- row_count:0:未修改数据 ; >0:表示修改的行数; <0: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; $$ -- 储存过程定义结束 DELIMITER ; SET @r_result = -3; -- 执行储存过程 CALL execute_seckill(1003, 13502178891, now(), @r_result); -- 获取结果 SELECT @r_result;
Mybatis调用存储过程其实和JDBC是一样的:

在使用存储过程的时候,我们需要4个参数,其实result是在存储过程中被赋值的。我们可以通过MapUtils来获取相对应的值。这是我之前没有接触过的。
最后,对于部署的系统架构应该是这样子的:
总结
花了点时间看了该视频教程,觉得还是学到了不少的东西的。之前没有接触过优化的相关问题,现在给我打开了思路,以及学到了不少的开发规范的问题,也是很赞的。如果是初学者的话是可以去学学的。