“五月榴花妖艳烘,绿杨带雨垂垂重。五月新丝缠角粽,金盘送。生绡画扇盘双凤。正是浴兰时节动。”正值端午佳节,实习公司也是例行放假三天以及给每一位员工发放了节日小礼品  😋。过完端午又将迎来​​618​​​活动专场,秒杀抢单活动也是此起彼伏,从而产生刺激性消费。由此不仅引出一个内心的小疑惑,商品的​​秒杀​​​又是怎么实现的呢?如果商品售卖量超过了​​秒杀​​的库存数又该如何解决呢?


Java设计与实现“秒杀”活动之抢粽子【完整版】_java



目录



🍒 难度分析

何为​​秒杀​​​,简单的理解为时间很短、速度很快。举一个常见的  🌰:比如某宝的​​618​​​活动场景,当大量的用户在短时间内涌入,瞬间流量巨大也就是​​高并发​​​场景。而秒杀活动其实是一个特别考验后台数据库以及缓存的业务,对于数据库、缓存的性能要求极高,如果系统的某个应用出现延迟反应,则就会出现用户的点击频繁,最坏的情况会造成​​雪崩​​,从而 造成系统垮掉。对于秒杀活动其实就是两个操作:

  • 商品库存减一
  • 秒杀成功后将用户添加进秒杀订单

🥝 项目回复(秒杀)

🍇 最终效果演示

Java设计与实现“秒杀”活动之抢粽子【完整版】_乐观锁_02

🍉 技术选型

  • 🍠 SpringBoot
  • 🍍 Thymeleaf
  • 🍌 Redis(事务)

🥕 项目需求分析

用户抢购秒杀商品,首先判断用户是否重复秒杀,如果重复秒杀则需要提示用户不能重复秒杀商品,否则查询库存,再进行库存的判断,如果库存数量大于0,则可以继续秒杀,否则提示秒杀结束,秒杀成功后库存减一,而用户则添加进​​Redis​​​缓存,最后离线同步数据库中。
Java设计与实现“秒杀”活动之抢粽子【完整版】_redis_03

🌏 项目搭建

① 创建Maven项目

引入相关依赖,构建所需文件目录

Java设计与实现“秒杀”活动之抢粽子【完整版】_乐观锁_04

② 编写配置文件

server:
port: 8888

spring:
redis:
# 服务地址
host: localhost
# 端口
port: 6379
# 数据库
database: 0
# 超时等待时间
connect-timeout: 10000ms
lettuce:
pool:
# 最大等待
max-active: 8
# 最大等待时间
max-wait: 10000ms
max-idle: 200
min-idle: 5

thymeleaf:
cache: false
prefix: classpath:/templates/

③ 编写Redis配置类

@Configuration
public class RedisConfig {
@Bean
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. 跳转到秒杀抢购页面的接口
@RequestMapping("/")
public String enterIndex(){
return "index";
}
2. 实现秒杀抢购业务

在执行秒杀抢购时,需要对秒杀操作添加事务处理,这样让我们的操作更有条理性,使用​​ Redis​​​ 事务还要配合​​Redis​​​的 ​​Watch ​​​实现乐观锁,​​Watch​​​ 命令可用于监视我们的库存数量 ,如果在事务执行之前库存数量被其他命令所改动,那么事务就会被打断,因此,在​​ Redis​​​ 中使用​​Watch​​给库存数目加乐观锁可以了。

3. 那么为什么使用事务呢?

想象一个场景:如果有很多人都知道你的账户,而且同时去参加​​618​​​秒杀活动,假设你账户上只有​​10000​​​元,你妈妈看上一件物品需要花费​​8000​​​元,你女朋友看上一件物品需要花费​​5000​​​元,而且你自己同时也看上了一件物品需要花费​​1000​​元,如果不采用事务处理会出现什么操作呢?

Java设计与实现“秒杀”活动之抢粽子【完整版】_java_05

当同时去消费的时候,结果是账户上最终会为一个负数,很显然银行账户是不会允许我们出现这种亏本的情况出现的,而我们采用了事务操作,就相当于使用了​​乐观锁​​的机制。

5. 浅识乐观锁

Java设计与实现“秒杀”活动之抢粽子【完整版】_spring boot_06

​乐观锁(Optimistic Lock)​​​,顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会去修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。​​乐观锁适用于多读的应用类型,这样可以提高吞吐量​​​。​​Redis就是利用这种check-and-set机制实现事务的​​。

通俗的来讲就是保证我们在同时进行秒杀抢购时不会使得我们的账户最终出现负数的情况,保证了数据的完整性。

6. 实际代码实现
@RequestMapping("/doGrabZz")
@ResponseBody
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() {
@Override
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. 前端实现

Java设计与实现“秒杀”活动之抢粽子【完整版】_乐观锁_07

8. 测试秒杀抢购结果

Java设计与实现“秒杀”活动之抢粽子【完整版】_java_08

⑤ ❗ ❗注意事项(测试秒杀前了解) ❗ ❗

  • 由于没有连接数据库进行持久化操作,所以需要在测试前在​​Redis​​中创建一些库存数据

Java设计与实现“秒杀”活动之抢粽子【完整版】_redis_09

  • 使用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() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
//开启事务
operations.multi();
redisTemplate.opsForValue().decrement(kcKey);
redisTemplate.opsForSet().add(userKey,uid);
//执行
return operations.exec();
}
});

新鲜出炉的代码将会及时更新到​​Gitee​​仓库

Java设计与实现“秒杀”活动之抢粽子【完整版】_乐观锁_10