Vejamos primeiro o bloqueio comum e o desempenho do pico de transação,
ilustrar:
1. Demora 100 milissegundos para executar o negócio seckill aqui
2. O computador está equipado com 16g de memória, 4 núcleos e 8 threads, cpu i7 7ª geração e o pool de conexão do banco de dados máximo = 20
@RequestMapping("/purchase2")
public ResultJson purchase2( Long productId){
int userId = new Random().nextInt(10000);
UserInfo.setUserId(userId);
RLock lock = redissonClient.getLock("LOCK");
lock.lock();
OrderVO purchase = null;
try {
purchase = tOrderService.purchase2(productId);
if(purchase.isSuccess()){
return new ResultJson<>(200, "成功下单", purchase);
}else{
return new ResultJson<>(500, purchase.getMsg(), purchase);
}
} catch (Exception e) {
e.printStackTrace();
}
lock.unlock();
return new ResultJson<>(500, "下单失败", null);
}
8.000 seckills simultâneos para 100 produtos
1. Bloqueio e transação comuns 100 commodities 8.000 simultaneidade, leva 13 segundos para armazenar 100 itens e a taxa de transferência é de 9 vezes por segundo. Aqui, o jmeter executa apenas 2.000 threads e para porque é muito lento para esperar.
1.000 commodities, 8.000 flash kills simultâneos
1. Bloqueio comum mais transação 1.000 itens 8.000 simultaneidade, leva 199 segundos para armazenar 1.000 itens Aqui, o jmeter executou apenas 2.000 threads e depois parou porque era muito lento e não queria esperar.
O seguinte é minha solução otimizada, personalizando anotações Seckkill e anotações ProductIdMark usando processamento aop
@RequestMapping("/purchase")
@Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
public ResultJson purchase(@ProductIdMark Long productId){
OrderVO purchase = null;
try {
purchase = tOrderService.purchase(productId);
if(purchase.isSuccess()){
return new ResultJson<>(200, "成功下单", purchase);
}else{
return new ResultJson<>(500, purchase.getMsg(), purchase);
}
} catch (Exception e) {
e.printStackTrace();
}
return new ResultJson<>(500, "下单失败", null);
}
8.000 seckills simultâneos para 100 produtos
1. Leva 4 segundos para armazenar 100 itens para 8.000 bloqueios simultâneos e transações para 100 itens , e a taxa de transferência é de 1.034 vezes por segundo.
8.000 seckills simultâneos para 1.000 produtos
1. Bloqueio comum mais transação 1.000 itens 8.000 simultaneidade, 1.000 itens precisam de 7 segundos para armazenar e a taxa de transferência é de 615 vezes por segundo
analisar
100 commodities antes da otimização: 13 segundos para armazenamento, 9/segundo para taxa de transferência
Depois que 100 itens são otimizados: o armazenamento leva 4 segundos e a taxa de transferência é de 1.034/s
1.000 commodities antes da otimização: 199 segundos para armazenamento, taxa de transferência de 9/segundo
Depois que 1.000 mercadorias são otimizadas: o armazenamento leva 7 segundos e a taxa de transferência é de 615/segundo
Pode-se ver que ainda há uma grande diferença
Ideia de implementação: todas as solicitações são interceptadas pelo aop e o aop armazena os usuários no redis, e a taxa de compra de 1.000 produtos é de 70%; então é a vez de 1500 O usuário concorre a 1000 produtos. Durante o aquecimento, divida 1000 produtos em 20 partes, ou seja, 50 produtos por peça. Armazene 20 produtos no depósito para usar como fechaduras lockName. Treinamento de rotação síncrona obtém o lockName no banco de dados e obtém o lockName correspondente. O inventário registrado pode ser deduzido. O processo geral é assim
A seguir está a implementação principal do aop:
@Aspect
@Configuration
public class SeckillAspect {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ProductsSubsectionService productsSubsectionService;
@Pointcut("@annotation(seckill)")
public void pointCut(Seckill seckill) {
}
private final String infoKey= "info:productId:";
private final String indexKey= "index:productId:";
private final Long time=60*10L;
@Around("pointCut(seckill)")
public Object around(ProceedingJoinPoint joinPoint,Seckill seckill) throws Throwable {
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
String appName = className +":"+ methodName;
Long productId = getProductId(joinPoint);
//模拟用户
int userId = new Random().nextInt(100000);
UserInfo.setUserId(userId);
//初始化商品数据
init(appName,productId);
if(stopRun(appName,productId)){
return new ResultJson<>(500, "商品售空", null);
}
//防止同一个用户使用外挂疯狂点击
RMap<Integer, Integer> map = redissonClient.getMap(appName+":userClicks");
RLock lockClicks = redissonClient.getLock("LOCK:"+appName+":5");
try {
lockClicks.lock();
map.put(userId,map.get(userId)+1);
}finally {
lockClicks.unlock();
}
if(map.get(userId) > 1){
return new ResultJson<>(500, "请勿重复提交", null);
}
RLock lock = redissonClient.getLock("LOCK:"+appName+":2");
lock.lock();
if(isUpdatePrimaryTable(appName,productId)){
updatePrimaryTable(seckill,productId);
}
String lockName = getLockName(appName, productId);
RLock lock4 = redissonClient.getLock(lockName);
try {
lock4.lock();
lock.unlock();
ProductsSubsection productsSubsection = productsSubsectionService.queryByLockMark(lockName);
if(productsSubsection.getNumber()==0){
return new ResultJson<>(500, "商品售空", null);
}
Object proceed = joinPoint.proceed();
productsSubsection.setNumber(productsSubsection.getNumber()-1);
productsSubsectionService.update(productsSubsection);
return proceed;
}finally {
lock4.unlock();
}
}
private Long getProductId(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
//一维数组是注解位置,二维数组是注解个数
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Long productId=null;
boolean noProductId = true;
for (int i = 0; i < parameterAnnotations.length; i++) {
Annotation[] annotation = parameterAnnotations[i];
for (Annotation var : annotation) {
if (var.annotationType().equals(ProductIdMark.class)) {
productId= (Long) args[i];
noProductId = false;
break;
}
}
}
if(noProductId){
throw new RuntimeException("形参未指定注解:ProductId");
}
return productId;
}
private boolean init(String appName, Long productId){
if(!redissonClient.getMap(appName).isEmpty()){
//已初始化
return false;
}
RLock lock = redissonClient.getLock("LOCK:"+appName+":2");
try {
lock.lock();
if(redissonClient.getMap(appName).isEmpty()){
List<ProductsSubsection> list = productsSubsectionService.queryByProdId(productId);
int inventory =0 ;
LinkedHashSet<String> locks = new LinkedHashSet<>();
for (ProductsSubsection subsection : list) {
inventory+=subsection.getNumber();
locks.add(subsection.getLockMark());
}
CommodityInfo info = new CommodityInfo();
info.setLockNames(locks);
info.setInventory(inventory);
RMap<String, Object> map = redissonClient.getMap(appName);
map.expire(time, TimeUnit.SECONDS);
String key= infoKey+ productId;
String key2= indexKey+ productId;
map.put(key,info);
map.put(key2,0);
}
}finally {
lock.unlock();
}
return true;
}
//统计用户点击数
private Integer userClicks(String appName,int size){
RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");
user.expire(time, TimeUnit.SECONDS);
RLock lock = redissonClient.getLock("LOCK:"+appName+":3");
int userLength = user.size();
if(userLength >= size){
return user.size();
}
try {
lock.lock();
if(user.size() < size){
//初始化用户点击次数
int userId = UserInfo.getUserId();
Integer clicks = user.get(userId);
if( clicks == null){
user.put(userId,0);
}else{
user.put(userId,clicks+1);
}
}
} finally {
lock.unlock();
}
return user.size();
}
private boolean stopRun(String appName,Long productId){
RMap<String, Object> map = redissonClient.getMap(appName);
map.expire(time, TimeUnit.SECONDS);
CommodityInfo info = (CommodityInfo) map.get( infoKey+ productId);
double size = info.getProbability() * info.getInventory();
RMap<Integer, Integer> user = redissonClient.getMap(appName+":userClicks");
user.expire(time, TimeUnit.SECONDS);
if(userClicks(appName, (int) size) >= size && !user.containsKey(UserInfo.getUserId())){
return true;
}
return false;
}
private String getLockName(String appName,Long productId){
String key= infoKey+ productId;
String key2= indexKey+ productId;
RMap<Object, Object> map = redissonClient.getMap(appName);
map.expire(time, TimeUnit.SECONDS);
CommodityInfo info = (CommodityInfo) map.get(key);
Integer index = (Integer) map.get(key2);
List<String> lockNamesList = new ArrayList<>(info.getLockNames());
map.put(key2,index+1);
return lockNamesList.get(index % lockNamesList.size());
}
private boolean isUpdatePrimaryTable(String appName,Long productId){
String key= infoKey+ productId;
String key2= indexKey+ productId;
RMap<Object, Object> map = redissonClient.getMap(appName);
map.expire(time, TimeUnit.SECONDS);
CommodityInfo info = (CommodityInfo) map.get(key);
Integer index = (Integer) map.get(key2);
return index==info.getInventory();
}
//更新主表
private void updatePrimaryTable(Seckill seckill,Long productId){
// 获取注解的值
String tableName = seckill.tableName();
String inventoryColumn = seckill.inventoryColumn();
String productIdColumn = seckill.productIdColumn();
System.out.println("开始更新"+tableName+"主表数据");
productsSubsectionService.updatePrimaryTable(tableName,inventoryColumn,productIdColumn,productId);
}
}
endereço do projeto:
Comum a todas as empresas seckill, desde que haja estoque na tabela de produtos, o ID do produto está correto
Regras de uso:
1. Crie uma tabela de segmentos
CREATE TABLE `products_subsection` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`prod_id` bigint(20) DEFAULT NULL,
`number` int(11) DEFAULT NULL,
`lock_mark` varchar(20) DEFAULT NULL,
`create_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4
2. Use a anotação Seckill e @ProductIdMark no método Controller
//商品表名,库存字段名,商品id名
@Seckill(tableName = "products", inventoryColumn = "prod_num" ,productIdColumn="id" )
//用于表示商品id
@ProductIdMark
efeito pós-visita
diagrama de arquitetura