秒杀的处理方案
秒杀技术实现核心思想是运用缓存减少数据库瞬间的访问压力。在秒杀时,首先会将数据库的秒杀商品同步到缓存中,用户从缓存中查询秒杀商品,抢购商品时减少缓存中的库存数量。产生的秒杀订单先写到缓存,付款成功后再写入数据库。
同步秒杀商品到redis
我们需要将正在秒杀的商品从数据库同步保存到redis中,在redis中
秒杀商品是以Hash类型保存,Hash的键是商品id,值是商品对象。
用户只能查询正在秒杀的商品 ( 开始时间 < 当前时间 < 结束时间,且库存 > 0 ) ,所以我们在redis中只保存正在秒杀的商品。由于每分钟都有商品开始秒杀,也有商品结束秒杀。所以需要定时查询数据库中正在秒杀的商品,同步到redis中。我们使用SpringTask技术,每分钟同步一次数据。
用户秒杀会修改redis中的商品库存,而此时mysql中的库存是没有修改的。等到下次同步数据的时候,redis中的库存数就又成了mysql中没有修改过的库存了。为了保证数据的同步,我们在将数据库数据同步到redis之前,先将redis中的商品库存数据同步到数据库中。
定时任务同步redis和数据库可参考示例代码:
/**
* 每分钟查询一次数据库,更新redis中的秒杀商品数据
* 条件为startTime < 当前时间 < endTime,库存大于0
*/
@Scheduled(cron = "0 * * * * *")
public void refreshRedis() {
// 将redis中秒杀商品的库存数据同步到mysql
List<SeckillGoods> seckillGoodsListOld = redisTemplate.boundHashOps("seckillGoods").values();
for (SeckillGoods seckillGoods : seckillGoodsListOld) {
// 在数据库中查询秒杀商品
SeckillGoods sqlSeckillGoods = seckillGoodsMapper.selectById(seckillGoods.getId());
// 修改秒杀商品的库存
sqlSeckillGoods.setStockCount(seckillGoods.getStockCount());
seckillGoodsMapper.updateById(sqlSeckillGoods);
}
// 1.查询数据库中正在秒杀的商品
QueryWrapper<SeckillGoods> queryWrapper = new QueryWrapper();
Date date = new Date();
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date);
queryWrapper.le("startTime", now) // 当前时间晚于开始时间
.ge("endTime", now) // 当前时间早于开始时间
.gt("stockCount", 0); // 库存大于0
List<SeckillGoods> seckillGoodsList = seckillGoodsMapper.selectList(queryWrapper);
// 2.删除之前的秒杀商品
redisTemplate.delete("seckillGoods");
// 3.保存现在正在秒杀的商品
for (SeckillGoods seckillGoods : seckillGoodsList) {
redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getGoodsId(), seckillGoods);
}
}
分页查询秒杀商品列表(返回有分页的格式)
将redis中存储的秒杀商品数据构造分页结构返回给前端可参考如下代码:
@Override
public Page<SeckillGoods> findPageByRedis(int page, int size) {
// 1. 查询所有秒杀商品
List<SeckillGoods> seckillGoodsList = redisTemplate.boundHashOps("seckillGoods").values();
// 2. 获取当前页商品列表
// 开始截取索引
int start = (page - 1) * size;
// 结束截取索引
int end = start + size > seckillGoodsList.size() ? seckillGoodsList.size():start + size;
// 获取当前页结果集
List<SeckillGoods> seckillGoods = seckillGoodsList.subList(start, end);
// 3. 构造页面对象
Page<SeckillGoods> seckillGoodsPage = new Page();
seckillGoodsPage.setCurrent(page) // 当前页
.setSize(size) // 每页条数
.setTotal(seckillGoodsList.size()) // 总条数
.setRecords(seckillGoods); //结果集
return seckillGoodsPage;
}
根据id查询秒杀商品
@Override
public SeckillGoods findSeckillGoodsByRedis(Long goodsId) {
return (SeckillGoods) redisTemplate.boundHashOps("seckillGoods").get(goodsId);
}
生成秒杀订单
为了让用户购买速度更快,秒杀商品时不会将商品添加到购物车,而是直接生成订单。并且由于访问量较大,为了避免数据库压力过大,我们会先将订单数据保存在redis当中,等用户支付完成后,再将redis中的订单数据保存到数据库中。
在用户成功秒杀下单后,商品库存减少,如果用户长时间不支付,则该商品始终被用户占据,其他用户也无法购买。我们需要给订单设置过期时间,过期后删除订单,回退商品库存。
创建订单简单示例代码
@Override
public Orders createOrder(Orders orders) {
// 1.生成订单对象
orders.setId(IdWorker.getIdStr()); // 手动生产订单id
orders.setStatus(1); // 订单状态未付款
orders.setCreateTime(new Date()); // 订单创建时间
orders.setExpire(new Date(new Date().getTime()+1000*60*5));
// 计算商品价格
CartGoods cartGoods = orders.getCartGoods().get(0);
Integer num = cartGoods.getNum();
BigDecimal price = cartGoods.getPrice();
BigDecimal sum = price.multiply(BigDecimal.valueOf(num));
orders.setPayment(sum);
// 2.减少秒杀商品库存
// 查询秒杀商品
SeckillGoods seckillGoods = findSeckillGoodsByRedis(cartGoods.getGoodId());
// 查询库存,库存不足抛出异常
Integer stockCount = seckillGoods.getStockCount();
if (stockCount <= 0){
throw new BusException(CodeEnum.NO_STOCK_ERROR);
}
// 减少库存
seckillGoods.setStockCount(seckillGoods.getStockCount() - cartGoods.getNum());
redisTemplate.boundHashOps("seckillGoods").put(seckillGoods.getGoodsId(),seckillGoods);
// 3.保存订单数据
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 设置订单一分钟过期
redisTemplate.opsForValue().set(orders.getId(),orders,1, TimeUnit.MINUTES);
/**
* 给订单创建副本,副本的过期时间长于原订单
* redis过期后触发过期事件时,redis数据已经过期,此时只能拿到key,拿不到value。
* 而过期事件需要回退商品库存,必须拿到value即订单详情,才能拿到商品数据,进行回退操作
* 我们保存一个订单副本,过期时间长于原订单,此时就可以通过副本拿到原订单数据
*/
redisTemplate.opsForValue().set(orders.getId()+"_copy",orders,2,TimeUnit.MINUTES);
return orders;
}
编写redis监听器,监听过期未支付订单 (RedisKeyExpirationListener.java和RedisListenerConfig.java)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* redis监听器
*/
@Configuration
public class RedisListenerConfig {
// 配置redis监听器,监听redis过期时间
@Bean
public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
订单过期后,关闭交易,回退商品库存
/**
* redis监听类继承KeyExpirationEventMessageListener
*/
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private SeckillService seckillService;
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
/**
* 订单过期后,关闭交易,回退商品库存
* @param message
* @param pattern
*/
@Override
public void onMessage(Message message, byte[] pattern) {
// 获取订单id
String orderId = message.toString();
// 拿到复制订单信息
Orders orders = (Orders) redisTemplate.opsForValue().get(orderId + "_copy");
Long goodId = orders.getCartGoods().get(0).getGoodId();//产品id
Integer num = orders.getCartGoods().get(0).getNum();//产品数据
// 查询秒杀商品
SeckillGoods seckillGoods = seckillService.findSeckillGoodsByRedis(goodId);
// 回退库存
seckillGoods.setStockCount(seckillGoods.getStockCount()+num);
redisTemplate.boundHashOps("seckillGoods").put(goodId,seckillGoods);
// 删除复制订单数据
redisTemplate.delete(orderId+"_copy");
}
}
支付秒杀订单
/**
* 支付秒杀订单
* @param id 订单id
* @return
*/
@GetMapping("/pay")
public BaseResult pay(String id){
// 支付秒杀订单
// 1.查询订单,设置相应数据
Orders orders = (Orders) redisTemplate.opsForValue().get(orderId);
if (orders == null){
throw new BusException(CodeEnum.ORDER_EXPIRED_ERROR);
}
orders.setStatus(2);
orders.setPaymentTime(new Date());
orders.setPaymentType(2); // 支付宝支付
// 2.从redis删除订单
redisTemplate.delete(orderId);
redisTemplate.delete(orderId+"_copy");
// 将订单存入数据库
orderService.add(orders);
return BaseResult.ok();
}