1、介绍mq
消息存储架构图中主要有下面三个跟消息存储相关的文件构成。
(1) CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G, 文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起始偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一个文件;
(2) ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能,由于RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行的,如果要遍历commitlog文件中根据topic检索消息是非常低效的。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引,保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量offset,消息大小size和消息Tag的HashCode值。consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:topic/queue/file三层组织结构,具体存储路径为:$HOME/store/consumequeue/{topic}/{queueId}/{fileName}。同样consumequeue文件采取定长设计,每一个条目共20个字节,分别为8字节的commitlog物理偏移量、4字节的消息长度、8字节tag hashcode,单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue文件大小约5.72M;
(3) IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。Index文件的存储位置是:$HOME/store/index/{fileName},文件名fileName是以创建时的时间戳命名的,固定的单个IndexFile文件大小约为400M,一个IndexFile可以保存 2000W个索引,IndexFile的底层存储设计为在文件系统中实现HashMap结构,故RocketMQ的索引文件其底层实现为hash索引。(通过消息的key或者id查找到的消息,hash的时间复杂度O(n)+O(1))
数据结构知识:
方法一:
方法二:
另一位老师讲解:也可以这样记hash, y轴,是一个弹簧,每个key压弹簧,使用同样的力度压弹簧,落的位置,就是存储的位置。
H(key)=hash(key), hash有对用的计算方法(比如直接定址,除留取余法,平方取中法)。
如果出现hash冲突(线性探测,二次探测,随机探测, 再哈希法, 链地址法)
时间复杂度:
链表: O(n)
冒泡: O(n^2)最坏情况下 ,最好O(n)
B+tree: O(logn)
在上面的RocketMQ的消息存储整体架构图中可以看出,RocketMQ采用的是混合型的存储结构,即为Broker单个实例下所有的队列共用一个日志数据文件(即为CommitLog)来存储。RocketMQ的混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中)针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。这里,RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。
1.2 页缓存与内存映射
页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。
在RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取,在page cache机制的预读取作用下,Consume Queue文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。而对于CommitLog消息存储的日志数据文件来说,读取消息内容时候会产生较多的随机访问读取,严重影响性能。如果选择合适的系统IO调度算法,比如设置调度算法为“Deadline”(此时块存储采用SSD的话),随机读的性能也会有所提升。
另外,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销),将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。
2、秒杀介绍
秒杀在很短的时间内要处理大量请求,需要高并发。
并发:多个任务在同一时间段内执行
并行:多个任务在同一时刻执行
这里主要目的是从接口上减少相应时间:
1、能异步就异步
2、减少IO(统一查询,统一写)
3、尽早return(做校验…)。
4、加锁粒度尽可能小
5、事务控制粒度尽可能小
…
两个程序(一个生产者,一个消费者)
流程:
1、用户进入秒杀接口,redis setnx 进行用户去重(判断用户是否买个这个商品),如果不存在,则进行预库减;如果存在则返回客户端。
进行预库减之后,会把相应的uk(唯一值uniqueKey=userId+goodsId)存到 redis中,供下次 用户去重进行判断。【redis 写8w左右,读11w左右。实际情况写6w左右,读8w左右】
2、用户秒杀到的订单存放到mq 进行异步处理。
3、消费服务,从mq拿到信息,进行数据库的扣减操作。
4、每次启动项目时,要进行同步redis库存。
redis 配置
redis配置:
package com.example.config;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.example.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Autowired
private final RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
redisTemplate.setConnectionFactory(this.redisConnectionFactory);
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(fastJsonRedisSerializer);
redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public StringRedisTemplate stringRedisTemplate() {
StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(this.redisConnectionFactory);
stringRedisTemplate.afterPropertiesSet();
return stringRedisTemplate;
}
@Bean
public RedisUtil redisUtil(RedisTemplate<String, Object> redisTemplate, StringRedisTemplate stringRedisTemplate) {
RedisUtil redisUtil = new RedisUtil();
redisUtil.setRedisTemplate(redisTemplate);
redisUtil.setStringRedisTemplate(stringRedisTemplate);
log.info("组件加载完成:redis工具类 ");
return redisUtil;
}
public RedisConfiguration(final RedisConnectionFactory redisConnectionFactory) {
this.redisConnectionFactory = redisConnectionFactory;
}
public RedisConnectionFactory getRedisConnectionFactory() {
return this.redisConnectionFactory;
}
// public String toString() {
// return "RedisConfiguration(redisConnectionFactory=" + this.getRedisConnectionFactory() + ")";
// }
}
redis 工具
package com.example.utils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
private RedisTemplate<String, Object> redisTemplate;
private StringRedisTemplate stringRedisTemplate;
public static final Integer GENERAL_CACHE_EXPIRE_DURATION = 2592000;
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
public void set(String k, Object v, long time) {
if (v instanceof String && this.stringRedisTemplate != null) {
this.stringRedisTemplate.opsForValue().set(k, (String) v);
} else {
this.redisTemplate.opsForValue().set(k, v);
}
if (time > 0L) {
this.redisTemplate.expire(k, time, TimeUnit.SECONDS);
}
}
public void set(String k, Object v) {
this.set(k, v, -1L);
}
public boolean contains(String key) {
return this.redisTemplate.hasKey(key);
}
public String get(String k) {
return this.stringRedisTemplate != null ? (String) this.stringRedisTemplate.opsForValue().get(k) : (String) this.redisTemplate.opsForValue().get(k);
}
public <T> T getObject(String k) {
ValueOperations<String, Object> valueOps = this.redisTemplate.opsForValue();
return (T) valueOps.get(k);
}
public void remove(String key) {
this.redisTemplate.delete(key);
}
public long getExpire(String key) {
return this.redisTemplate.getExpire(key);
}
public Set<String> keys(String pattern) {
return this.redisTemplate.keys(pattern);
}
public Long increment(String key, long delta, long time) {
Long val = this.redisTemplate.opsForValue().increment(key, delta);
if (val.equals(1L) && time > 0L) {
this.redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return val;
}
public Double increment(String key, double delta) {
return this.redisTemplate.opsForValue().increment(key, delta);
}
public boolean hset(String key, Map<String, Object> map) {
try {
this.redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception var4) {
var4.printStackTrace();
return false;
}
}
public Object hget(String key, String item) {
return this.redisTemplate.opsForHash().get(key, item);
}
public Map<Object, Object> getKeys(String key) {
return this.redisTemplate.opsForHash().entries(key);
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lgetSize(String key) {
return this.redisTemplate.opsForList().size(key);
}
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始下标
* @param end 结束下标 当等于-1时,表示所有
* @return
*/
public List<Object> lget(String key, long start, long end) {
return this.redisTemplate.opsForList().range(key, start, end);
}
/**
* 设置list缓存
*
* @param key 键
* @param value 值
* @param time 失效时间
*/
public void lset(String key, Object value, long time) {
this.redisTemplate.opsForList().rightPush(key, value);
if (time > 0L) {
this.redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
}
}
3、生产者-消费者搭建
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.6.13</version>
</dependency>
<!-- 使用的一个对象池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.17</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
数据库:
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for goods
-- ----------------------------
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`goods_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`price` decimal(10, 2) NULL DEFAULT NULL,
`stocks` int(255) NULL DEFAULT NULL,
`status` int(255) NULL DEFAULT NULL,
`pic` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
`update_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '小米12s', 4999.00, 1000, 2, 'xxxxxx', '2023-02-23 11:35:56', '2023-02-23 16:53:34');
INSERT INTO `goods` VALUES (2, '华为mate50', 6999.00, 10, 2, 'xxxx', '2023-02-23 11:35:56', '2023-02-23 11:35:56');
INSERT INTO `goods` VALUES (3, '锤子pro2', 1999.00, 100, 1, NULL, '2023-02-23 11:35:56', '2023-02-23 11:35:56');
-- ----------------------------
-- Table structure for order_records
-- ----------------------------
DROP TABLE IF EXISTS `order_records`;
CREATE TABLE `order_records` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NULL DEFAULT NULL,
`order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`goods_id` int(11) NULL DEFAULT NULL,
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
生产者:
配置文件yml
# 应用服务 WEB 访问端口
server:
port: 8081
tomcat:
threads:
# 最大线程数: 400
max: 400
rocketmq:
name-server: 192.168.101.128:9876
producer:
# 发送消息超时时间: 3s
send-message-timeout: 3000
#失败重试次数同步次数
retry-times-when-send-failed: 2
# 失败重试次数同步次数异步
retry-times-when-send-async: 2
# 4194304 4M,发送最大消息字节
max-message-size: 4194304
#在内部发送失败时是否重试其他代理,这个参数在有多个broker时才生效
retry-next-server: true
access-key: rocketmq2
secret-key: 12345678
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=UTC
username: root
password: 123456
#配置redis
redis:
# 超时时间
timeout: 10000ms
# 连接的主机ip
host: 127.0.0.1
port: 6379
# redis使用的0号数据库
database: 0
password:
main:
allow-bean-definition-overriding: true
测试类:
package com.example.controller;
import com.alibaba.cola.dto.Response;
import com.alibaba.cola.dto.SingleResponse;
import com.alibaba.fastjson.JSON;
import com.example.service.GoodsService;
import com.example.utils.RedisUtil;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 1、用户去重,
* 2、预减库存
* 3、存放到mq中
* @date 2024/10/6 18:24
*/
@RestController
@RequestMapping("/seckill")
public class SeckillController {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private RedisUtil redisUtil;
@Autowired
private GoodsService goodsService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@RequestMapping("/doSeckill")
public Response doSeckill(Integer goodsId,Integer userId) {
String uk=userId+"-"+goodsId;
//去重处理
Boolean flag = redisTemplate.opsForValue().setIfAbsent(uk, "");
if (!flag) {
System.out.println("已经秒杀过了");
return Response.buildFailure("-1","已经秒杀过了,请您参与其他商品抢购");
}
//TODO 预减库存,查-改-更新 不安全,这里直接采用原子性减库存
//假设已经同步到数据库了,key=goodsId:xxx,value=库存量,这是redis中的库存量
Long count = redisTemplate.opsForValue().decrement("goodsId:" + goodsId);
if (count < 0) {
return Response.buildFailure("-1","该商品已经被抢完,请下次早点来哦O(∩_∩)O");
}
//TODO:存放到mq中
//进行json发送到消费者
String json = JSON.toJSONString(uk);
//异步发送
rocketMQTemplate.asyncSend("seckillTopic", json, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("消息发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("消息发送失败"+throwable.getMessage());
}
});
return SingleResponse.of("拼命抢购中,请稍后去订单中心查看");
}
}
package com.example.config;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.domain.Goods;
import com.example.service.GoodsService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
/**
* @author GJ
* @date 2024/10/7 17:10
*/
@Component
public class DataSynConfig implements InitializingBean {
@Autowired
private GoodsService goodsService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 搞一个定时任务,每天10点开启执行一次。
*
*
* spring bean的生命周期[实例化-new,属性赋值,初始化(前 PostConstruct/中 InitializingBean 接口/ 后 BeanPostProcessor),使用,销毁]
* 在当前对象 实例化完以后
* 属性注入以后
* 执行 PostConstruct 注解的方法
*/
// 初始化方法,启动自动加载和这个方法
@PostConstruct
public void afterPropertiesSet(){
//查询数据库,加载数据到缓存中
LambdaQueryWrapper<Goods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Goods::getStatus,1);
List<Goods> goodsList = goodsService.list(queryWrapper);
System.out.println("数据库查询出来数据--->"+goodsList);
System.out.println(">>>>>>>>>>>>>>>>>>>>开始加载数据到redis中>>>>>>>>>>>>>>>>>>>>>>>>");
for (Goods goods : goodsList) {
redisTemplate.opsForValue().set("goodsId:"+goods.getId(),goods.getStocks());
}
System.out.println(">>>>>>>>>>>>>>>>>>>>结束加载数据到redis中>>>>>>>>>>>>>>>>>>>>>>>>");
}
}
消费者:
配置文件yml
server:
port: 8899
tomcat:
threads:
# 最大线程数: 400
max: 400
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/seckill?serverTimezone=UTC
username: root
password: 123456
#配置redis
redis:
# 超时时间
timeout: 10000ms
# 连接的主机ip
host: 127.0.0.1
port: 6379
# redis使用的0号数据库
database: 0
password:
main:
allow-bean-definition-overriding: true
rocketmq:
name-server: 127.0.0.1:9876
consumer:
access-key: rocketmq2
secret-key: 12345678
redis 配置和工具和生产者一样
消费监听
package com.example.listener;
import com.example.service.GoodsService;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import javax.annotation.Resource;
/**
* @author GJ
* @date 2024/10/8 17:06
* description: 秒杀监听
*/
@RocketMQMessageListener(topic = "seckill-topic-consumer",
consumerGroup = "group-order",
messageModel = MessageModel.CLUSTERING,
consumeThreadNumber = 24)
public class SeckillListener implements RocketMQListener<MessageExt> {
@Resource
private GoodsService goodsService;
@Override
public void onMessage(MessageExt message) {
System.out.println("收到消息:" + message);
byte[] body = message.getBody();
String msg = new String(body);
// 将msg转换为json对象,并解析
String[] split = msg.split("-");
Integer userId = Integer.valueOf(split[0]);
Integer goodsId = Integer.valueOf(split[1]);
System.out.println("用户id:" + userId + ",商品id:" + goodsId);
// 调用业务层方法,完成秒杀操作
goodsService.kill(userId, goodsId);
}
}
将主键自增的id,初始化:
TRUNCATE ‘order’
package com.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.domain.Goods;
import com.example.domain.OrderRecords;
import com.example.mapper.GoodsMapper;
import com.example.mapper.OrderRecordsMapper;
import com.example.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
*
*/
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods>
implements GoodsService {
@Autowired
private OrderRecordsMapper orderRecordsMapper;
@Autowired
private GoodsMapper goodsMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void kill(Integer userId, Integer goodsId) {
/**
* 1、判断库存是否充足
* 2、扣减库存
* 3、创建订单
*/
synchronized (this) {
LambdaQueryWrapper<Goods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Goods::getId, goodsId)
.eq(Goods::getStatus, 1)
.gt(Goods::getStocks, 0);
Goods goods = goodsMapper.selectOne(queryWrapper);
Integer stocks = goods.getStocks();
if (stocks <= 0) {
throw new RuntimeException("库存不足");
}
// 扣减库存
goods.setStocks(--stocks);
goods.setUpdate_time(new Date());
int i = goodsMapper.updateById(goods);
// 创建订单
if (i > 0) {
OrderRecords orderRecords = new OrderRecords();
orderRecords.setUser_id(userId);
orderRecords.setGoods_id(goodsId);
orderRecords.setCreate_time(new Date());
orderRecordsMapper.insert(orderRecords);
}
}
}
}
可重复度:会将数据拍个照。
事务之间相互隔离。
A 事务先释放锁,然后在提交。释放锁时,B进来,读取原来数据,后来A 提交的事务-导致幻读。
(解决这个问题,先提交然后再释放锁 即 事务由锁包裹)。
方案一:
package com.example.listener;
import com.example.service.GoodsService;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
*
* @date 2024/10/8 17:06
* description: 秒杀监听
*/
@Component
@RocketMQMessageListener(topic = "seckill-topic",
consumerGroup = "group-order",
messageModel = MessageModel.CLUSTERING,
consumeThreadNumber = 24)
public class SeckillListener implements RocketMQListener<MessageExt> {
@Resource
private GoodsService goodsService;
@Override
public void onMessage(MessageExt message) {
System.out.println("收到消息:" + message);
byte[] body = message.getBody();
String msg = new String(body);
// 将msg转换为json对象,并解析
String[] split = msg.split("-");
Integer userId = Integer.valueOf(split[0]);
Integer goodsId = Integer.valueOf(split[1]);
System.out.println("用户id:" + userId + ",商品id:" + goodsId);
// 调用业务层方法,完成秒杀操作
synchronized (this) {
goodsService.kill(userId, goodsId);
}
}
}
结果:
使用jemter进行压测:
满足不了分布式需求
方案二:
package com.example.listener;
import com.example.service.GoodsService;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @date 2024/10/8 17:06
* description: 秒杀监听
*/
@Component
@RocketMQMessageListener(topic = "seckill-topic",
consumerGroup = "group-order",
messageModel = MessageModel.CLUSTERING,
consumeThreadNumber = 24)
public class SeckillListener implements RocketMQListener<MessageExt> {
@Resource
private GoodsService goodsService;
@Override
public void onMessage(MessageExt message) {
System.out.println("收到消息:" + message);
byte[] body = message.getBody();
String msg = new String(body);
// 将msg转换为json对象,并解析
String[] split = msg.split("-");
Integer userId = Integer.valueOf(split[0]);
Integer goodsId = Integer.valueOf(split[1]);
System.out.println("用户id:" + userId + ",商品id:" + goodsId);
// 调用业务层方法,完成秒杀操作
goodsService.kill(userId, goodsId);
}
}
把锁方法放到数据层面。
a=a-1 操作数据库时,mysql行锁。
package com.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.domain.Goods;
import com.example.domain.OrderRecords;
import com.example.mapper.GoodsMapper;
import com.example.mapper.OrderRecordsMapper;
import com.example.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
*
*/
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods>
implements GoodsService {
@Autowired
private OrderRecordsMapper orderRecordsMapper;
@Autowired
private GoodsMapper goodsMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void kill(Integer userId, Integer goodsId) {
/**
* 1、判断库存是否充足
* 2、扣减库存
* 3、创建订单
*/
//使用mysql行锁,
LambdaUpdateWrapper<Goods> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper
.set(Goods::getUpdate_time, new Date())
.setSql("stocks=stocks-1 where stocks-1>=0 and id=" + goodsId);
int i = goodsMapper.update(null, updateWrapper);
System.out.println("更新成功" + i);
// 创建订单
if (i > 0) {
OrderRecords orderRecords = new OrderRecords();
orderRecords.setUser_id(userId);
orderRecords.setGoods_id(goodsId);
orderRecords.setCreate_time(new Date());
orderRecordsMapper.insert(orderRecords);
}
}
}
结果:
不适合用于 并发量特别大的场景,几百, 1000左右并发量 可以使用方案二。
方案三:
并发量很大呢?
使用redisson 锁
package com.example.listener;
import com.example.service.GoodsService;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author GJ
* @date 2024/10/8 17:06
* description: 秒杀监听
*/
@Component
@RocketMQMessageListener(topic = "seckill-topic",
consumerGroup = "group-order",
messageModel = MessageModel.CLUSTERING,
consumeThreadNumber = 24)
public class SeckillListener implements RocketMQListener<MessageExt> {
@Resource
private GoodsService goodsService;
@Resource
private RedisTemplate redisTemplate;
int maxTime=20000;
@Override
public void onMessage(MessageExt message) {
System.out.println("收到消息:" + message);
byte[] body = message.getBody();
String msg = new String(body);
// 将msg转换为json对象,并解析
String[] split = msg.split("-");
Integer userId = Integer.valueOf(split[0]);
Integer goodsId = Integer.valueOf(split[1]);
System.out.println("用户id:" + userId + ",商品id:" + goodsId);
int currentTime = 0;
//最外层的就是自旋锁,最内层就是加锁机制;currentTime> maxTime写成true 也可以,一直自旋。目前写的是20000/200=100次 也可以在
//currentTime +=200,睡几秒钟。自旋的慢点
while(currentTime> maxTime){
Boolean flag = redisTemplate.opsForValue().setIfAbsent("lock:" + goodsId, "lock");
// 加锁
if (flag){
try{
// 调用业务层方法,完成秒杀操作
goodsService.kill(userId, goodsId);
} finally {
// 释放锁
redisTemplate.delete("lock:" + goodsId);
}
}else {
currentTime +=200;
}
}
}
}
package com.example.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.domain.Goods;
import com.example.domain.OrderRecords;
import com.example.mapper.GoodsMapper;
import com.example.mapper.OrderRecordsMapper;
import com.example.service.GoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
*
*/
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods>
implements GoodsService {
@Autowired
private OrderRecordsMapper orderRecordsMapper;
@Autowired
private GoodsMapper goodsMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public void kill(Integer userId, Integer goodsId) {
/**
* 1、判断库存是否充足
* 2、扣减库存
* 3、创建订单
*/
LambdaQueryWrapper<Goods> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Goods::getId, goodsId)
.eq(Goods::getStatus, 1)
.gt(Goods::getStocks, 0);
Goods goods = goodsMapper.selectOne(queryWrapper);
Integer stocks = goods.getStocks();
if (stocks <= 0) {
throw new RuntimeException("库存不足");
}
// 扣减库存
goods.setStocks(--stocks);
goods.setUpdate_time(new Date());
int i = goodsMapper.updateById(goods);
// 创建订单
if (i > 0) {
OrderRecords orderRecords = new OrderRecords();
orderRecords.setUser_id(userId);
orderRecords.setGoods_id(goodsId);
orderRecords.setCreate_time(new Date());
orderRecordsMapper.insert(orderRecords);
}
}
}
结果: