这个项目是从github上拉下来的一个项目,借鉴项目上的代码来实现的秒杀系统,主要有
基于Mysql悲观锁,乐观锁实现,利用redis的watch监控,以及利用AtomicInteger的CAS机制特性等四种方法来实现高并发高负载的场景,也算是补充一下这块知识的空白。
1 ) @ControllerAdvice
全局捕获异常类,主要用于配合@ExceptionHandler,只要作用在@RequestMapping上,所有的异常都会被捕获,如果使用的话返回的异常类型一般需要加上@ResponseBody,因为返回的数据类型是json格式的。
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
//不加ResponseBody的话会会报错
@ExceptionHandler(value = SecKillException.class)
@ResponseBody
public Message handleSecKillException(SecKillException secKillException){
log.info(secKillException.getSecKillEnum().getMessage());
return new Message(secKillException.getSecKillEnum());
}
}
2 ) @Data
使用这个注解,Getter,Setter,equals,canEqual,hasCode,toString等方法会在编译时自动加进去
3 ) @NoArgsConstructor
使用后创建一个无参构造函数
4 ) @AllArgsConstructor
使用后添加一个构造函数,该构造函数含有所有已声明字段属性参数
5 ) @PostConstruct
被注解的方法,在对象加载完依赖后执行,只执行一次
6 ) @Qualifier
表明那个参数才是我们所需要的,需要注意的是@Qualifier的参数名称为我们之前定义的注解的名称之一
7 ) @Value("${spring.datasource.url}")
加载properties文件中对应的字段
8 )@Primary
当一个接口有多个实现时,使用这个注解就可以实现默认采取它进行注入
9)@Scope
scope是一个非常关键的概念,定义了用户在spring容器中的生命周期,也可以理解为对象在spring容器中的创建方式
a singleton (单一实例)
此取值时表明容器中创建时只存在一个实例,所有引用此bean都是单一实例。
此外,singleton类型的bean定义从容器启动到第一次被请求而实例化开始,只要容器不销毁或退出,该类型的bean的单一实例就会一直存活
b prototype
spring容器在进行输出prototype的bean对象时,会每次都重新生成一个新的对象给请求方,虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不在拥有当前对象的引用,请求方需要自己负责当前对象后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回请求方该对象的一个新的实例之后,就由这个对象“自生自灭”
c 还有session,global session,request等三种类型 这里就不详讲
用到的sql语句是这一句
update product set stock=stock-1
where id=#{id} and stock>0
根据Mysql的知识可以知道,update行会给指定的记录加上记录锁,因此会封锁索引记录
上面的语句它会在 id相等的那一行的索引记录上锁,防止其他事务的插入更新。
主要看一下service层的方法
@Transactional
public SecKillEnum handleByPessLockInMySQL(Map<String, Object> paramMap) {
Jedis jedis = redisCacheHandle.getJedis();
Record record;
Integer userId = (Integer) paramMap.get("userId");
Integer productId = (Integer) paramMap.get("productId");
User user = secKillMapper.getUserById(userId);
Product product = secKillMapper.getProductById(productId);
/**
* 拿到用户所买商品在redis中对应的key
*/
String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
//判断该用户是否重复购买该商品
boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
if (isBuy) {
log.error("用户:" + user.getUsername() + "重复购买商品" + product.getProductName());
throw new SecKillException(SecKillEnum.REPEAT);
}
/**
* 判断该商品的库存 利用update行实现悲观锁 这里应该取消事务的自动提交功能
*/
boolean secKillSuccess = secKillMapper.updatePessLockInMySQL(product);
if (!secKillSuccess) {
log.error("商品:" + product.getProductName() + "库存不足!");
throw new SecKillException(SecKillEnum.LOW_STOCKS);
}
long result = jedis.sadd(hasBoughtSetKey, user.getId().toString());
if (result > 0) {
record = new Record(null, user, product, SecKillEnum.SUCCESS.getCode(), SecKillEnum.SUCCESS.getMessage(), new Date());
log.info(record.toString());
boolean insertFlag = secKillMapper.insertRecord(record);
if (insertFlag) {
log.info("用户:" + user.getUsername() + "秒杀商品:" + product.getProductName() + "成功!");
return SecKillEnum.SUCCESS;
} else {
log.error("系统错误!");
throw new SecKillException(SecKillEnum.SYSTEM_EXCEPTION);
}
} else {
log.error("用户:" + user.getUsername() + "重复秒杀商品" + product.getProductName());
throw new SecKillException(SecKillEnum.REPEAT);
}
}
注:@Transactional默认的是该方法执行完事务才进行提交
sql语句
update product set stock=#{stock},version=version+1
where id=#{id} AND version=#{version}
在数据库中添加版本号字段,当version相同时允许修改库存
@Transactional
public SecKillEnum handleByPosiLockInMySQL(Map<String, Object> paramMap) {
Jedis jedis = redisCacheHandle.getJedis();
Record record = null;
Integer userId = (Integer) paramMap.get("userId");
Integer productId = (Integer) paramMap.get("productId");
User user = secKillMapper.getUserById(userId);
Product product = secKillMapper.getProductById(productId);
String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
if (isBuy) {
log.error("用户:" + user.getUsername() + "重复购买商品" + product.getProductName());
throw new SecKillException(SecKillEnum.REPEAT);
}
//手动库存减一
int lastStock = product.getStock() - 1;
if (lastStock >= 0) {
product.setStock(lastStock);
/**
* 修改库存在version相同的情况下
*/
boolean secKillSuccess = secKillMapper.updatePosiLockInMySQL(product);
if (!secKillSuccess) {
log.error("用户:" + user.getUsername() + "秒杀商品" + product.getProductName() + "失败!");
throw new SecKillException(SecKillEnum.FAIL);
}} else {
log.error("商品:" + product.getProductName() + "库存不足!");
throw new SecKillException(SecKillEnum.LOW_STOCKS);
}
long addResult = jedis.sadd(hasBoughtSetKey, user.getId().toString());
if (addResult > 0) {
record = new Record(null, user, product, SecKillEnum.SUCCESS.getCode(), SecKillEnum.SUCCESS.getMessage(), new Date());
log.info(record.toString());
boolean insertFlag = secKillMapper.insertRecord(record);
if (insertFlag) {
log.info("用户:" + user.getUsername() + "秒杀商品" + product.getProductName() + "成功!");
return SecKillEnum.SUCCESS;
} else {
throw new SecKillException(SecKillEnum.SYSTEM_EXCEPTION);
}
} else {
log.error("用户:" + user.getUsername() + "重复秒杀商品:" + product.getProductName());
throw new SecKillException(SecKillEnum.REPEAT);
}
}
/**
* redis的watch监控
* @param paramMap
* @return
*/
public SecKillEnum handleByRedisWatch(Map<String, Object> paramMap) {
Jedis jedis = redisCacheHandle.getJedis();
Record record;
Integer userId = (Integer) paramMap.get("userId");
Integer productId = (Integer)paramMap.get("productId");
User user = secKillMapper.getUserById(userId);
Product product = secKillMapper.getProductById(productId);
/**
* 获得该产品的键值对
*/
String productStockCacheKey = product.getProductName()+"_stock";
/**
* 拿到用户所买商品在redis中对应的key
*/
String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
/**
* 开启watch监控
* 可以决定事务是执行还是回滚
* 它首先会去比对被 watch 命令所监控的键值对,如果没有发生变化,那么它会执行事务队列中的命令,
* 提交事务;如果发生变化,那么它不会执行任何事务中的命令,而去事务回滚。无论事务是否回滚,
* Redis 都会去取消执行事务前的 watch 命令
*/
jedis.watch(productStockCacheKey);
boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
if (isBuy){
log.error("用户:"+user.getUsername()+"重复购买商品"+product.getProductName());
throw new SecKillException(SecKillEnum.REPEAT);
}
String stock = jedis.get(productStockCacheKey);
if (Integer.parseInt(stock) <= 0) {
log.error("商品:"+product.getProductName()+"库存不足!");
throw new SecKillException(SecKillEnum.LOW_STOCKS);
}
//开启redis事务
Transaction tx = jedis.multi();
//库存减一
tx.decrBy(productStockCacheKey,1);
//执行事务
List<Object> resultList = tx.exec();
if(resultList == null || resultList.isEmpty()){
jedis.unwatch();
//watch监控被更改过----物品抢购失败;
log.error("商品:"+product.getProductName()+",watch监控被更改,物品抢购失败");
throw new SecKillException(SecKillEnum.FAIL);
}
//添加到已买队列
long addResult = jedis.sadd(hasBoughtSetKey,user.getId().toString());
if(addResult>0){
//秒杀成功
record = new Record(null,user,product,SecKillEnum.SUCCESS.getCode(),SecKillEnum.SUCCESS.getMessage(),new Date());
//添加record到rabbitmq消息队列
rabbitMQSender.send(JSON.toJSONString(record));
return SecKillEnum.SUCCESS;
}else{
//重复秒杀
//这里抛出RuntimeException异常,redis的decr操作并不会回滚,所以需要手动incr回去
jedis.incrBy(productStockCacheKey,1);
throw new SecKillException(SecKillEnum.REPEAT);
}
}
这里是rabbitmq的可可靠确认模式
@Slf4j
@Component
public class RabbitMQSender implements RabbitTemplate.ConfirmCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
public void send(String message){
rabbitTemplate.setConfirmCallback(this);//指定 ConfirmCallback
// 自定义消息唯一标识
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
/**
* 发送消息
*/
rabbitTemplate.convertAndSend("seckillExchange", "seckillRoutingKey", message, correlationData);
}
/**
* 生产者发送消息后的回调函数
* @param correlationData
* @param b
* @param s
*/
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
log.info("callbakck confirm: " + correlationData.getId());
if(b){
log.info("插入record成功,更改库存成功");
}else{
log.info("cause:"+s);
}
}
}
@Transactional
public SecKillEnum handleByAtomicInteger(Map<String, Object> paramMap) {
Jedis jedis = redisCacheHandle.getJedis();
Record record;
Integer userId = (Integer) paramMap.get("userId");
Integer productId = (Integer)paramMap.get("productId");
User user = secKillMapper.getUserById(userId);
Product product = secKillMapper.getProductById(productId);
String hasBoughtSetKey = SecKillUtils.getRedisHasBoughtSetKey(product.getProductName());
//判断是否重复购买
boolean isBuy = jedis.sismember(hasBoughtSetKey, user.getId().toString());
if (isBuy){
log.error("用户:"+user.getUsername()+"重复购买商品"+product.getProductName());
throw new SecKillException(SecKillEnum.REPEAT);
}
AtomicInteger atomicInteger = atomicStock.getAtomicInteger(product.getProductName());
int stock = atomicInteger.decrementAndGet();
if(stock < 0){
log.error("商品:"+product.getProductName()+"库存不足, 抢购失败!");
throw new SecKillException(SecKillEnum.LOW_STOCKS);
}
long result = jedis.sadd(hasBoughtSetKey,user.getId().toString());
if (result > 0){
record = new Record(null,user,product,SecKillEnum.SUCCESS.getCode(),SecKillEnum.SUCCESS.getMessage(),new Date());
log.info(record.toString());
boolean insertFlag = secKillMapper.insertRecord(record);
if (insertFlag) {
//更改物品库存
secKillMapper.updateByAsynPattern(record.getProduct());
log.info("用户:"+user.getUsername()+"秒杀商品"+product.getProductName()+"成功!");
return SecKillEnum.SUCCESS;
} else {
log.error("系统错误!");
throw new SecKillException(SecKillEnum.SYSTEM_EXCEPTION);
}
} else {
log.error("用户:"+user.getUsername()+"重复秒杀商品"+product.getProductName());
atomicInteger.incrementAndGet();
throw new SecKillException(SecKillEnum.REPEAT);
}
}
相关知识点来自的微博:
https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651961471&idx=1&sn=da257b4f77ac464d5119b915b409ba9c&chksm=bd2d0da38a5a84b5fc1417667fe123f2fbd2d7610b89ace8e97e3b9f28b794ad147c1290ceea&scene=21#wechat_redirect
代码借鉴:https://github.com/SkyScraperTwc/SecKillDesign
源码:https://github.com/OnlyGky/SecondKill