并发问题分析
超卖:超过了库存还可以卖给用户
缓存 Redis -> “分布式锁”
锁 : 乐观 悲观
【悲观】
以悲观的态度,在修改数据之前,把数据锁住,使⽤完才释放,下一个使用时同理
数据库所使用的“行锁” 即为悲观锁
【乐观】
以乐观的态度,对数据进行并行处理,在数据提交时验证,有没有发生冲突
表A
id name version
1 zhangsan 1
lisi 2
操作1 读出数据 记录版本 然后修改name为lisi -》 验证版本⼀致 可以修改
操作2 读出数据 记录版本 然后修改name为wangwu -》 验证版本不⼀致 不可以修改
git -> 版本冲突
当前状态 商品列表功能 1.0
ATuo 继续商品列表 进阶版商品列表 2.0
Dmc 开发商品详情 基于1.0 提交了3.0
提交时 有和2.0中同时修改的文件出现冲突
list.html -> dmc增加了商品详情的链接
-> 重构了页面增加了样式
手动合并冲突 给出最终版本 -> 提交成功
超卖问题分析
Redis
环境准备
1) redis服务能够启动
虚拟机要注意的问题: 是否开启虚拟化网络设置(桥接+正确的⽹卡名)
是否开启虚拟化,通过任务管理器查看
问题: 代码中启动n个线程时 在操作系统中会占用多少个线程?
A
2) 主机能够连接redis服务
找到redis的安装地址 按照指定的配置文件启动
cd /usr/local/bin
./redis-server ~/testRedis/redis.conf
因为redis.conf配置了重要的几项,能够让远程连接此服务
#确认远程可访问 且 保护模式打开
bind 0.0.0.0
protected-mode yes
直接关闭防火墙 / 打开防火墙,开放redis的端口号
firewall-cmd —add-service=http —permanent
firewall-cmd —add-port=6379/tcp —permanent
#重启防火墙
firewall-cmd —reload
#查看开放的端口
firewall-cmd —permanent —zone=public —list-ports
3) redis-client 的jar包使用
http://www.redis.cn/clients.html#java
Jedis : 老牌客户端 支持基础的redis命令
Redisson : 分布式客户端 支持可扩展的数据结构
Lettuce : 高级Redis客户端 可以支持线程安全、异步响应、集群哨兵等
Lettuce是springboot2.0后版本所使用的客户
springboot整合
spring-data-redis 通过简单的配置来访问redis服务
https://spring.io/projects/spring-data-redis
类似jdbc, 通过redisTemplate来进行相关的增删改查操作
此处也是使用了连接池的设计理念 (池的本质就是复用)
1)增加对redis相关依赖的引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
2)编写redis配置
spring:
redis:
host: 192.168.43.251
port: 6379
#密码可以自定义,但不管有没有设置密码,此配置项都必须有
password:
#redis数据库索引,0-16,默认为0
database: 1
3)额外编写redis连接池的配置
#声明连接池参数
#最大连接数 默认8,设置为负数表示无限制
spring.redis.jedis.pool.max-active=8
#最大阻塞等待时间 默认-1
spring.redis.jedis.pool.max-wait=-1
#最大空闲连接
spring.redis.jedis.pool.max-idle=8
#最小空闲连接
spring.redis.lettuce.pool.min-idle=0
#连接超时时间
spring.redis.timeout=1000
4) 在spring容器中注入参数
package com.duing.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@PropertySource("classpath:redis.properties")
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
RedisTemplate<String,Object> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
//声明对key和value的序列化方式
StringRedisSerializer keySerializer=new StringRedisSerializer();
GenericFastJsonRedisSerializer valueSerializer=new GenericFastJsonRedisSerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashKeySerializer(keySerializer);
redisTemplate.setHashValueSerializer(valueSerializer);
return redisTemplate;
}
}
5)封装了redis工具类
package com.duing.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
//设置缓存
public void set(String key,Object value){
redisTemplate.opsForValue().set(key, value);
}
public void set(String key,Object value,long timeout){
redisTemplate.opsForValue().set(key, value);
}
public Object get(String key){
if (!redisTemplate.hasKey(key)){
return null;
}
return redisTemplate.opsForValue().get(key);
}
}
6) 在service中进行调用
package com.duing.service.impl;
import com.duing.mapper.SeckillGoodsMapper;
import com.duing.model.SeckillGoods;
import com.duing.service.RedisService;
import com.duing.util.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisUtil redisUtil;
@Autowired
private SeckillGoodsMapper mapper;
@Override
public void initData(String goodsId, int stockNum) {
/**
* 存储数据时需要设计好key的规则,例如根据商品id找到唯一库存
* 秒杀活动开始前工作人员先通过后台管理系统将数据导入数据库在。
* 再确保redis中存储一份数据用于秒杀时刻(额外同步数据功能)
* 从指定mysql表中取出需要的数据初始化到redis中
*/
redisUtil.set(goodsId+"_stockNum",stockNum);
}
@Override
public void initData() {
List<SeckillGoods> list= mapper.getAllSeckillGoods();
for (SeckillGoods goods:list){
String goodsId=goods.getGoodsId();
redisUtil.set(goodsId+"_stockNum",goods.getStockNum());
SimpleDateFormat format=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
redisUtil.set(goodsId+"_startTime",format.format(goods.getStartTime()));
System.out.println(format.format(goods.getStartTime()));
}
}
}
开发秒杀请求所需要处理的逻辑
public String seckill(String userId, String goodsId) throws ParseException {
String start = (String) redisUtil.get(goodsId + "_startTime");
String end = (String) redisUtil.get(goodsId + "_endTime");
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date startTime = format.parse(start);
Date endTime = format.parse(end);
//首先判断秒杀状态
if (startTime == null || new Date().before(startTime)) {
return "秒杀还未开始";
} else if (new Date().after(endTime)) {
return "秒杀已结束";
}else {
//判断秒杀商品库存是否充足
int stockNum=(int) redisUtil.get(goodsId+"_stockNum");
if (stockNum<1){
return "商品库存不足";
}else {
//判断用户是否曾经秒杀成功过
if (redisUtil.get(goodsId+"_"+userId)!=null){
return "每人限购一件";
}
redisUtil.set(goodsId+"_"+userId,1);
return userId+"用户,秒杀成功!";
}
}
}