并发问题分析

超卖:超过了库存还可以卖给用户

缓存 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增加了商品详情的链接
-> 重构了页面增加了样式
手动合并冲突 给出最终版本 -> 提交成功

超卖问题分析

image.png

Redis

环境准备

1) redis服务能够启动
虚拟机要注意的问题: 是否开启虚拟化网络设置(桥接+正确的⽹卡名)
image.png
image.png
是否开启虚拟化,通过任务管理器查看
image.png
问题: 代码中启动n个线程时 在操作系统中会占用多少个线程?
A n

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相关依赖的引用

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.apache.commons</groupId>
  7. <artifactId>commons-pool2</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>com.alibaba</groupId>
  11. <artifactId>fastjson</artifactId>
  12. <version>1.2.47</version>
  13. </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+"用户,秒杀成功!";
            }
        }
    }