1. 并发量大了容易内存溢出
2. 数据不安全,容易丢失
package com.hmdp.service.impl;
import cn.hutool.core.thread.NamedThreadFactory;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisIdGenerator;
import com.hmdp.utils.RedisUtils;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/**
*
* 服务实现类:实现异步秒杀抢购卷
*
*
*
* @author TH
* @since 2022-04-02
*/
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Autowired
private SeckillVoucherServiceImpl seckillVoucherService;
//全局唯一id生成器
@Autowired
private RedisIdGenerator redisIdGenerator;
//redis的常用方法工具类
@Autowired
private RedisUtils redisUtils;
//redisson
@Autowired
private RedissonClient redissonClient;
/**
* 读取lua脚本的DefaultRedisScript的初始化
*/
private final static DefaultRedisScript<Long> SECKILL_VOUCHER;
static {
SECKILL_VOUCHER = new DefaultRedisScript<>();
//返回值类型,注意redis返回的数值类型(Integer)java必须用Long类型来接收,否则一定报错的。
SECKILL_VOUCHER.setResultType(Long.class);
//读取lua资源的方式
SECKILL_VOUCHER.setLocation(new ClassPathResource("seckillvoucher.lua"));
}
/**
* 定义阻塞队列
*/
private BlockingQueue<VoucherOrder> voucherOrderQueueTask = new ArrayBlockingQueue<>(1024 * 1024);
/**
* 手动定义核心线程-10,最大线程-10的 线程池
*/
private final static ExecutorService SECKILL_ORDER_EXECUTOR = new ThreadPoolExecutor(10, 10,
0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(),
new NamedThreadFactory("执行秒杀订单消息队列", false));
/**
* 定义一个内部类,实现 Runnable接口,执行创建订单的任务
*/
private class VoucherOrderHeader implements Runnable {
@Override
public void run() {
//如果队列中没有数据那么take()会卡在这个地方。有的时候再执行。
while (true) {
try {
//1.获取队列的订单信息
VoucherOrder voucherOrder = voucherOrderQueueTask.take();
//2.创建订单、扣减库存
createVoucherOrderCluster(voucherOrder);
} catch (Exception e) {
log.error("队列处理订单异常", e);
}
}
}
}
/**
* 定一个初始化方法:@PostConstruct在该类初始化完毕后就要执行该方法。
* 任务必须在活动之前就要开始执行队列中的订单创建任务方法
*/
@PostConstruct
private void init() {
//在初始化时执行任务。
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHeader());
}
/**
* 秒杀卷的抢购方法
*
* @param voucherId
* @return
*/
@Override
public Result seckillVoucher(Long voucherId) {
//1.判断用户是否登录过
Long userId = UserHolder.getUser().getId();
if (userId == null) {
return Result.fail("对不起,请先登录");
}
//2.定义key的集合
List<String> keysList = new ArrayList<>(2);
//2.1设置库存的key
String seckillVoucherKey = RedisConstants.SECKILL_VOUCHER_KEY + voucherId;
keysList.add(seckillVoucherKey);
//2.2 设置下单用户的key
String seckillUserorderKey = RedisConstants.SECKILL_USERORDER_KEY + voucherId;
keysList.add(seckillUserorderKey);
//3.执行lua脚本,返回结果信息
Long resultInfo = redisUtils.execute(SECKILL_VOUCHER, keysList, userId.toString());
//4.根据返回结果信息判断该用户是否具有秒杀资格。或者说是否拿到了秒杀的令牌。
switch (resultInfo.intValue()) {
case -1:
//4.1 redis缓存中没有库存的缓存数据
return Result.fail("活动未开始");
case 1:
//4.2 redis缓存中库存为0
return Result.fail("优惠卷已抢空,欢迎下次光临!");
case 2:
//4.3 重复下单
return Result.fail("对不起,一个用户只能抢购一次!");
case 0:
//4.4 获取到抢购成功的令牌,执行把订单信息加入阻塞队列:v1.0采用阻塞队列来创建订单
return addQueue(voucherId, userId);
default:
//4.5 其他,说明lua脚本有问题
return Result.fail("lua脚本结果错误!");
}
}
/**
* 获取到抢购成功的令牌,执行把订单信息加入阻塞队列:采用阻塞队列来创建订单
*
* @param voucherId
* @param userId
* @since v1.0
* @return
*/
private Result addQueue(Long voucherId, Long userId) {
// 6.2创建订单信息
long id = redisIdGenerator.nextId("order");
VoucherOrder voucherOrder = new VoucherOrder();
voucherOrder.setId(id);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
//7.把订单信息加入到阻塞队列中
voucherOrderQueueTask.add(voucherOrder);
//8.返回订单id
return Result.ok(id);
}
/**
* 创建订单信息:用互斥锁作为兜底,确保lua脚本抢购逻辑没有问题
*
* @param voucherOrder
* @return
*/
@Transactional(rollbackFor = Exception.class)
public void createVoucherOrderCluster(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
Long voucherId = voucherOrder.getVoucherId();
String lockKey = RedisConstants.LOCK_SECKILL_VOUCHER + userId;
RLock redissonClientLock = redissonClient.getLock(lockKey);
//1.:使用redisson的重入锁来实现。无参时,默认锁的有效期是30s。比较建议使用无参、因为有看门狗机制,
// 可以业务处理期间刷新有效期时间
boolean lockSuccess = redissonClientLock.tryLock();
//1.1判断该用户是否多次购买:目的是为了在用户还没有执行下单成功(存入数据库)。即:还处于程序中时同一用户请求的另一个线程插队执行下单。
if (!lockSuccess) {
//直接返回失败,有些情况是递归重试,直到该用户成功为止。
log.error("对不起,一个用户只能抢购一次!");
return;
}
//1.2再次判断该用户是否多次购买:目的是为了下单成功,数据已经写入到表了,此时已经释放了锁,那么上一步互斥锁就没有意义了。
try {
Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
if (count > 0) {
log.error("对不起,一个用户只能抢购一次!");
return;
}
//2.充足时,
// 2.1扣减库存
boolean deductionFlag = seckillVoucherService.update().setSql("stock=stock-1")
.eq("voucher_id", voucherId).gt("stock", 0)
.update();
if (!deductionFlag) {
//扣减库存失败
log.error("优惠卷已抢空,欢迎下次光临!");
return;
}
// 2.2创建订单信息
save(voucherOrder);
} finally {
//3.使用redisson的方式释放锁
redissonClientLock.unlock();
}
}
}
---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by TH
--- DateTime: 2022/4/5 21:51
---
---1.定义参数
local userId=ARGV[1];
--local voucherId=ARGV[2];
--某抢购卷的库存数量key如:seckillvoucher:stock:12 固定部分+秒杀卷的id,
--如果不想传key的参数,也可以在这儿拼接库存key,秒杀用户key。
--如:local seckillVoucherKey="seckillvoucher:stock:" .. voucherId;
local seckillVoucherKey=KEYS[1];
--下单的用户key:seckillUser:order:12
local seckillUserOrderKey=KEYS[2];
---2.判断是否存在库存的缓存数据
local stockExists=redis.call("exists",seckillVoucherKey);
if tonumber(stockExists)==0 then
--2.1 没有库存缓存数据,返回-1,表示活动未开始
return -1;
end
---3.获取库存数量
local stock=redis.call("get",seckillVoucherKey);
---4.判断库存是否充足'
if tonumber(stock)< 1 then
--4.1库存不足返回1:表示活动已结束
return 1
end
---5.判断用户是否已经下单,即判断用户是否存在于set集合中
local isUserExist=redis.call("sismember",seckillUserOrderKey,userId);
--不管是userOrderKey还是userId 不存在于缓存中都返回isUserExist==0
if tonumber(isUserExist)==1 then
--5.1 存在用户,表示已经下过单了
return 2;
end
---6.该用户获取到了抢购的资格
--6.1 减少库存
redis.call("decr",seckillVoucherKey);
--6.2 把获取资格的用户加入到userOrderKey中去
redis.call("sadd",seckillUserOrderKey,userId);
return 0;
A:Reddisson的bean配置类
package com.hmdp.config;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.URL;
/**
* reddisson的bean配置类
* @author TH
* @date 2022/4/1
*/
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
//1.配置类
Config config = new Config();
try {
//读取配置文件
URL url = RedissonConfig.class.getClassLoader().getResource("redisson-config.yaml");
config = Config.fromYAML(url);
//直接写死的方式
//config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("redis6379").setDatabase(0);
} catch (Exception e) {
e.printStackTrace();
}
//创建RedissonClient对象
return Redisson.create(config);
}
}
B:Redisson的相关配置的yaml文件
#redisson的相关配置
singleServerConfig:
idleConnectionTimeout: 10000 #连接空闲超时,单位:毫秒
connectTimeout: 10000 #连接超时,单位:毫秒 同节点建立连接时的等待超时。时间单位是毫秒。
timeout: 3000 #命令等待超时,单位:毫秒 等待节点回复命令的时间。该时间从命令发送成功时开始计时。
retryAttempts: 3 #命令失败重试次数
retryInterval: 1500 #命令重试发送时间间隔,单位:毫秒
password: "redis123" # redis密码
subscriptionsPerConnection: 5 #单个连接最大订阅数量
clientName: null #redis 客户端名称
address: "redis://127.0.0.1:6379" #redis地址
subscriptionConnectionMinimumIdleSize: 1 #发布和订阅连接的最小空闲连接数
subscriptionConnectionPoolSize: 50 #发布和订阅连接池大小
connectionMinimumIdleSize: 32 #最小空闲连接数
connectionPoolSize: 64 #连接池大小 默认值:64
database: 1 #数据库编号
dnsMonitoringInterval: 5000 #DNS监测时间间隔,单位:毫秒
threads: 0
nettyThreads: 0
C:常量定义
package com.hmdp.utils;
/**
* Redis常量的 定义
*/
public class RedisConstants {
/**
* 登录发送验证码的缓存前缀key以及TTL
*/
public static final String LOGIN_CODE_KEY = "login:code:";
public static final Long LOGIN_CODE_TTL = 5L;
/**
* 登录成功后的一个token缓存前缀key以及TTL
*/
public static final String LOGIN_USER_KEY = "login:token:";
public static final Long LOGIN_USER_TTL = 30L;
/**
* 店铺详情的缓存前缀key以及两种TTL
*/
public static final String CACHE_SHOP = "cache:shop:";
public static final Long CACHE_SHOP_TTL = 30L;
public static final Long CACHE_SHOP_NULL_TTL = 5L;
/**
* 缓存垫布类型的缓存KEY,全称
*/
public static final String CACHE_SHOPTYPE_LIST = "cache:shopType:list";
/**
* 店铺详情互斥锁:练习缓存穿透、击穿、雪崩使用的,以及过期时间TTL
*/
public static final String LOCK_SHOP = "lock:shop:";
public static final Long LOCK_SHOP_TTL = 3L;
/**
* 秒杀优惠卷互斥锁的以及TTL
*/
public static final String LOCK_SECKILL_VOUCHER = "lock:voucher:";
public static final Long LOCK_SECKILL_VOUCHER_TTL = 5L;
/**
* 秒杀卷库存key前缀:练习阻塞队列的时候秒杀下单
*/
public static final String SECKILL_VOUCHER_KEY = "seckillvoucher:stock:";
/**
* 秒杀下单用户的key前缀:练习阻塞队列的时候秒杀下单
*/
public static final String SECKILL_USERORDER_KEY = "seckillUser:order:";
/**
* 消息队列的全key
*/
public static final String STREAMS_VOUCHER_ORDER="streams.voucher.order";
/**
* 消费者组的名称
*/
public static final String GROUP_NAME="g1";
/**
* 消费者的名称
*/
public static final String CONSUMER_NAME="c1";
}
E:ID生成策略——基于redis生成全局唯一ID,单体、集群、分布式均适用:请点我…
F:Redis常用方法工具类:请点我…
G:Redis自定义redisTemplate序列化配置:请点我…