“五月榴花妖艳烘,绿杨带雨垂垂重。五月新丝缠角粽,金盘送。生绡画扇盘双凤。正是浴兰时节动。”正值端午佳节,实习公司也是例行放假三天以及给每一位员工发放了节日小礼品 😋。过完端午又将迎来618
活动专场,秒杀抢单活动也是此起彼伏,从而产生刺激性消费。由此不仅引出一个内心的小疑惑,商品的秒杀
又是怎么实现的呢?如果商品售卖量超过了秒杀
的库存数又该如何解决呢?
目录
- 1. 跳转到秒杀抢购页面的接口
- 2. 实现秒杀抢购业务
- 3. 那么为什么使用事务呢?
- 5. 浅识乐观锁
- 6. 实际代码实现
- 7. 前端实现
- 8. 测试秒杀抢购结果
🍒 难度分析
何为秒杀
,简单的理解为时间很短、速度很快。举一个常见的 🌰:比如某宝的618
活动场景,当大量的用户在短时间内涌入,瞬间流量巨大也就是高并发
场景。而秒杀活动其实是一个特别考验后台数据库以及缓存的业务,对于数据库、缓存的性能要求极高,如果系统的某个应用出现延迟反应,则就会出现用户的点击频繁,最坏的情况会造成雪崩
,从而 造成系统垮掉。对于秒杀活动其实就是两个操作:
- 商品库存减一
- 秒杀成功后将用户添加进秒杀订单
🥝 项目回复(秒杀)
🍇 最终效果演示
🍉 技术选型
- 🍠 SpringBoot
- 🍍 Thymeleaf
- 🍌 Redis(事务)
🥕 项目需求分析
用户抢购秒杀商品,首先判断用户是否重复秒杀,如果重复秒杀则需要提示用户不能重复秒杀商品,否则查询库存,再进行库存的判断,如果库存数量大于0,则可以继续秒杀,否则提示秒杀结束,秒杀成功后库存减一,而用户则添加进Redis
缓存,最后离线同步数据库中。
🌏 项目搭建
① 创建Maven项目
引入相关依赖,构建所需文件目录
② 编写配置文件
server
port8888
spring
redis
# 服务地址
host localhost
# 端口
port6379
# 数据库
database0
# 超时等待时间
connect-timeout 10000ms
lettuce
pool
# 最大等待
max-active8
# 最大等待时间
max-wait 10000ms
max-idle200
min-idle5
thymeleaf
cachefalse
prefix classpath /templates/
③ 编写Redis配置类
public class RedisConfig {
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
//key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
//value序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//hash类型key的序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//hash类型value的序列化
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
}
④ 编写Controller前端控制器
首先创建一个DoKillController
类
1. 跳转到秒杀抢购页面的接口
"/")(
public String enterIndex(){
return "index";
}
2. 实现秒杀抢购业务
在执行秒杀抢购时,需要对秒杀操作添加事务处理,这样让我们的操作更有条理性,使用 Redis
事务还要配合Redis
的 Watch
实现乐观锁,Watch
命令可用于监视我们的库存数量 ,如果在事务执行之前库存数量被其他命令所改动,那么事务就会被打断,因此,在 Redis
中使用Watch
给库存数目加乐观锁可以了。
3. 那么为什么使用事务呢?
想象一个场景:如果有很多人都知道你的账户,而且同时去参加618
秒杀活动,假设你账户上只有10000
元,你妈妈看上一件物品需要花费8000
元,你女朋友看上一件物品需要花费5000
元,而且你自己同时也看上了一件物品需要花费1000
元,如果不采用事务处理会出现什么操作呢?
当同时去消费的时候,结果是账户上最终会为一个负数,很显然银行账户是不会允许我们出现这种亏本的情况出现的,而我们采用了事务操作,就相当于使用了乐观锁
的机制。
5. 浅识乐观锁
乐观锁(Optimistic Lock)
,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会去修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量
。Redis就是利用这种check-and-set机制实现事务的
。
通俗的来讲就是保证我们在同时进行秒杀抢购时不会使得我们的账户最终出现负数的情况,保证了数据的完整性。
6. 实际代码实现
"/doGrabZz")(
public Boolean doGrabZz(HttpServletRequest request){
String gid = request.getParameter("gid");
String uid = UUID.randomUUID().toString().substring(1, 5);
//1. 拼接key
// 库存key
String kcKey = "sk:"+gid+":qt";
// 秒杀用户的key
String userKey = "sk:"+gid+":user";
//监视库存
redisTemplate.watch(kcKey);
//获取库存 如果库存为null,秒杀还未开始
String kc = String.valueOf(redisTemplate.opsForValue().get(kcKey));
if (kc == null){
System.out.println("秒杀还未开始,请稍等...");
return false;
}
//判断用户是否重复秒杀
if (redisTemplate.opsForSet().isMember(userKey,uid)){
System.out.println("已经秒杀成功,不能重复秒杀");
return false;
}
//判断如果商品数量,库存数量小于1,秒杀结束
if (Integer.parseInt(kc) <= 0){
System.out.println("秒杀已经结束");
return false;
}
List<Object> txRes = (List<Object>) redisTemplate.execute(new SessionCallback() {
public Object execute(RedisOperations operations) throws DataAccessException {
//开启事务
operations.multi();
redisTemplate.opsForValue().decrement(kcKey);
redisTemplate.opsForSet().add(userKey,uid);
//执行
return operations.exec();
}
});
if (txRes == null || txRes.size() == 0){
System.out.println("秒杀失败了");
return false;
}
System.out.println("秒杀成功了");
return true;
}
}
注: 为了方便演示,此案例并没有连接数据库进行操作,而是创建一些假数据进行测试
7. 前端实现
8. 测试秒杀抢购结果
⑤ ❗ ❗注意事项(测试秒杀前了解) ❗ ❗
- 由于没有连接数据库进行持久化操作,所以需要在测试前在
Redis
中创建一些库存数据
- 使用redis使用事务时出现
ERR EXEC without MULTI
错误示例:
//秒杀过程 使用事务
redisTemplate.multi();
redisTemplate.opsForValue().decrement(kcKey);
redisTemplate.opsForSet().add(userKey,uid);
//执行
List<Object> res = redisTemplate.exec();
执行上述代码则会出现问题
根据查阅官方文档可知:RedisTemplate
并不支持如上代码操作:
List<Object> txRes = (List<Object>) redisTemplate.execute(new SessionCallback() {
public Object execute(RedisOperations operations) throws DataAccessException {
//开启事务
operations.multi();
redisTemplate.opsForValue().decrement(kcKey);
redisTemplate.opsForSet().add(userKey,uid);
//执行
return operations.exec();
}
});
新鲜出炉的代码将会及时更新到Gitee
仓库