秒杀系统高并发优化学习(一)

扩展阅读

环境配置

创建一个Maven项目

SpringBoot集成MyBatis

使用 druid数据库连接池

【pom.xml】 添加依赖

......
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.9.RELEASE</version>
</parent>
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
</properties>
    
<dependencies>
    <!--boot-->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <!-- thymeleaf模板引擎-->
    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
    <!--mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.19</version>
    </dependency>

    <!--druid-->
    <dependency>
    	<groupId>com.alibaba</groupId>
    	<artifactId>druid-spring-boot-starter</artifactId>
    	<version>1.1.10</version>
    </dependency>
    
    <!--mybatis-->
    <dependency>
    	<groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>1.3.1</version>
    </dependency>
</dependencies>
  ......

【application.properties】

# thymeleaf
spring.thymeleaf.prefix=classpath:/templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.cache=false
spring.thymeleaf.content-type=text/html
spring.thymeleaf.enabled=true
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.mode=HTML5

# mybatis
mybatis.config-location=classpath:mybatis-config.xml
mybatis.mapper-locations= = classpath:com/hjx/mapper/*.xml

#druid
spring.datasource.url=jdbc:mysql://localhost:3306/miaosha?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
spring.datasource.druid.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.druid.filters=stat
spring.datasource.druid.maxActive=2
spring.datasource.druid.initialSize=1
spring.datasource.druid.maxWait=60000
spring.datasource.druid.minIdle=1
spring.datasource.druid.timeBetweenEvictionRunsMillis=60000
spring.datasource.druid.minEvictableIdleTimeMillis=300000
spring.datasource.druid.validationQuery=select 'x'
spring.datasource.druid.testWhileIdle=false
spring.datasource.druid.testOnBorrow=false
spring.datasource.druid.testOnReturn=false
spring.datasource.druid.poolPreparedStatements=true
spring.datasource.druid.maxOpenPreparedStatements=20

【mybatis-config.xml】

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!--默认大小写自动转换-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!--自动匹配批处理-->
        <setting name="defaultExecutorType" value="BATCH"/>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <!--设置别名后就可以使用resultType配置属性信息-->
    <typeAliases>
        <package name="com.hjx.pojo"/>
    </typeAliases>
</configuration>

【Result.java】封装一个结果类,用来返回格式统一的json

package com.hjx.result;

public class Result<T> {
   private int code;
   private String msg;
   private T data;

   /**
    * 成功时候的调用
    * */
   public static <T> Result<T> success(T data){
      return new  Result<T>(data);
   }
   
   /**
    * 失败时候的调用
    * */
   public static <T> Result<T> error(CodeMsg cm){
      return new  Result<T>(cm);
   }
   
   private Result(T data) {
      this.code = 0;
      this.msg = "success";
      this.data = data;
   }
   
   private Result(CodeMsg cm) {
      if(cm == null) {
         return;
      }
      this.code = cm.getCode();
      this.msg = cm.getMsg();
   }

   public int getCode() {
      return code;
   }
   public String getMsg() {
      return msg;
   }
   public T getData() {
      return data;
   }
}

【CodeMsg.java】封装一个错误信息结果类,用于错误消息返回

package com.hjx.result;

public class CodeMsg {
   private int code;
   private String msg;
   
   //通用异常
   public static CodeMsg SUCCESS = new CodeMsg(0, "success");
   public static CodeMsg SERVER_ERROR = new CodeMsg(500100, "服务端异常");
   //登录模块 5002XX
   
   //商品模块 5003XX
   
   //订单模块 5004XX
   
   //秒杀模块 5005XX
   
   
   private CodeMsg(int code, String msg) {
      this.code = code;
      this.msg = msg;
   }
   
   public int getCode() {
      return code;
   }
   public String getMsg() {
      return msg;
   }
}

项目结构:

请添加图片描述

下面测试连接是否成功:

【User.java】 建一个数据库映射的实体类

package com.hjx.pojo;
public class User {
    private int id;
    private String name;
    ......
}

【UserMapper.java】 mapper 接口

package com.hjx.mapper;

import com.hjx.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {
    @Select("select * from user where id = #{id}")
    User getUserById(@Param("id") int id);
}

【UserService.java】 简单写个服务层

@Service()
public class UserService {
    @Autowired
    UserMapper userMapper;

    public User getUserById(int id){
        return userMapper.getUserById(id);
    }
    public List<User> getUserList(){
        return userMapper.getUserList();
    }
}

【UserController.java】 Controller层写个接口

@RestController
public class UserController {
    @Autowired
    UserService userService;
    
    @GetMapping("/getUserById")
    public Result<User> getUserById(){
        User user = userService.getUserById(1);
        return Result.success(user);
    }
}

启动程序,访问接口
请添加图片描述

日志配置

【application.properties】

#日志框架
logging.config=classpath:logback-spring.xml

【logback-spring.xml】

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration  scan="true" scanPeriod="10 seconds">

    <!--<include resource="org/springframework/boot/logging/logback/base.xml" />-->

    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="D:/logs/miaosha" />

    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>


    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>info</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>


    <!--输出到文件-->

    <!-- 时间滚动输出 level为 DEBUG 日志 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_debug.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <fileNamePattern>${log.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>


    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
        以及指定<appender>。<logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
              如果未设置此属性,那么当前logger将会继承上级的级别。
        addtivity:是否向上级logger传递打印信息。默认是true。
    -->
    <!--<logger name="org.springframework.web" level="info"/>-->
    <!--<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>-->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
     -->


    <!--
        root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
        不能设置为INHERITED或者同义词NULL。默认是DEBUG
        可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->

    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <logger name="com.nmys.view" level="debug"/>
    </springProfile>

    <root level="info">
        <appender-ref ref="CONSOLE" />
        <appender-ref ref="DEBUG_FILE" />
        <appender-ref ref="INFO_FILE" />
        <appender-ref ref="WARN_FILE" />
        <appender-ref ref="ERROR_FILE" />
    </root>

    <!--生产环境:输出到文件-->
    <!--<springProfile name="pro">-->
    <!--<root level="info">-->
    <!--<appender-ref ref="CONSOLE" />-->
    <!--<appender-ref ref="DEBUG_FILE" />-->
    <!--<appender-ref ref="INFO_FILE" />-->
    <!--<appender-ref ref="ERROR_FILE" />-->
    <!--<appender-ref ref="WARN_FILE" />-->
    <!--</root>-->
    <!--</springProfile>-->

</configuration>

@Transactional

多条db操作时,有一条报错,都事物回滚

集成Redis (jedis驱动)

【pom.xml】 添加依赖

<!--连接redis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

<!-- json序列化-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.38</version>
</dependency>

【application.properties】 加入redis的配置信息 https://blog.csdn.net/qq_33326449/article/details/80457571

#redis
spring.redis.host=192.168.0.104
spring.redis.port=6379
spring.redis.timeout=3
spring.redis.password=123456
spring.redis.pool.max-active=10
spring.redis.pool.max-idle=10
spring.redis.pool.max-wait=3

【RedisConfig.java】配置实体类

package com.hjx.redis;
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    private int timeout;//秒
    @Value("${spring.redis.password}")
    private String password;
    @Value("${spring.redis.pool.max-active}")
    private int poolMaxTotal;
    @Value("${spring.redis.pool.max-idle}")
    private int poolMaxIdle;
    @Value("${spring.redis.pool.max-wait}")
    private int poolMaxWait;//秒

    public String getHost() {
        return host;
    }
    public void setHost(String host) {
        this.host = host;
    }
    public int getPort() {
        return port;
    }
    public void setPort(int port) {
        this.port = port;
    }
    public int getTimeout() {
        return timeout;
    }
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public int getPoolMaxTotal() {
        return poolMaxTotal;
    }
    public void setPoolMaxTotal(int poolMaxTotal) {
        this.poolMaxTotal = poolMaxTotal;
    }
    public int getPoolMaxIdle() {
        return poolMaxIdle;
    }
    public void setPoolMaxIdle(int poolMaxIdle) {
        this.poolMaxIdle = poolMaxIdle;
    }
    public int getPoolMaxWait() {
        return poolMaxWait;
    }
    public void setPoolMaxWait(int poolMaxWait) {
        this.poolMaxWait = poolMaxWait;
    }

}

【RedisPoolFactory.java】 将jedis注入, 接着我们就可以在其他类中获取到JedisPool类的信息

package com.hjx.redis;
@Service
public class RedisPoolFactory {
	@Autowired
	RedisConfig redisConfig;
	
	@Bean
	public JedisPool JedisPoolFactory() {
		JedisPoolConfig poolConfig = new JedisPoolConfig();
		poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
		poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
		poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);  //配置里是秒,这里是毫秒 所以*1000
		JedisPool jp = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(),
				redisConfig.getTimeout()*1000, redisConfig.getPassword(), 0);
		return jp;
	}
}

【RedisSerice.java】 创建一个工具类 实现几个常用操作

KeyPrefix 是前缀用于防止相同key名互相覆盖

package com.hjx.redis;
@Service
public class RedisService {
	
	@Autowired
	JedisPool jedisPool;
	
	/**
	 * 获取单个对象
	 * */
	public <T> T get(KeyPrefix prefix, String key,  Class<T> clazz) {
		 Jedis jedis = null;
		 try {
			 jedis =  jedisPool.getResource();
			 //生成真正的key
			 String realKey  = prefix.getPrefix() + key;
			 String  str = jedis.get(realKey);
			 T t =  stringToBean(str, clazz);
			 return t;
		 }finally {
			  returnToPool(jedis);
		 }
	}
	
	/**
	 * 设置对象
	 * */
	public <T> boolean set(KeyPrefix prefix, String key,  T value) {
		 Jedis jedis = null;
		 try {
			 jedis =  jedisPool.getResource();
			 String str = beanToString(value);
			 if(str == null || str.length() <= 0) {
				 return false;
			 }
			//生成真正的key
			 String realKey  = prefix.getPrefix() + key;
			 int seconds =  prefix.expireSeconds();
			 if(seconds <= 0) {
				 jedis.set(realKey, str);
			 }else {
				 jedis.setex(realKey, seconds, str);
			 }
			 return true;
		 }finally {
			  returnToPool(jedis);
		 }
	}
	
	/**
	 * 判断key是否存在
	 * */
	public <T> boolean exists(KeyPrefix prefix, String key) {
		 Jedis jedis = null;
		 try {
			 jedis =  jedisPool.getResource();
			//生成真正的key
			 String realKey  = prefix.getPrefix() + key;
			return  jedis.exists(realKey);
		 }finally {
			  returnToPool(jedis);
		 }
	}
	
	/**
	 * 增加值
	 * */
	public <T> Long incr(KeyPrefix prefix, String key) {
		 Jedis jedis = null;
		 try {
			 jedis =  jedisPool.getResource();
			//生成真正的key
			 String realKey  = prefix.getPrefix() + key;
			return  jedis.incr(realKey);
		 }finally {
			  returnToPool(jedis);
		 }
	}
	
	/**
	 * 减少值
	 * */
	public <T> Long decr(KeyPrefix prefix, String key) {
		 Jedis jedis = null;
		 try {
			 jedis =  jedisPool.getResource();
			//生成真正的key
			 String realKey  = prefix.getPrefix() + key;
			return  jedis.decr(realKey);
		 }finally {
			  returnToPool(jedis);
		 }
	}
	
	private <T> String beanToString(T value) {
		if(value == null) {
			return null;
		}
		Class<?> clazz = value.getClass();
		if(clazz == int.class || clazz == Integer.class) {
			 return ""+value;
		}else if(clazz == String.class) {
			 return (String)value;
		}else if(clazz == long.class || clazz == Long.class) {
			return ""+value;
		}else {
			return JSON.toJSONString(value);
		}
	}

	@SuppressWarnings("unchecked")
	private <T> T stringToBean(String str, Class<T> clazz) {
		if(str == null || str.length() <= 0 || clazz == null) {
			 return null;
		}
		if(clazz == int.class || clazz == Integer.class) {
			 return (T)Integer.valueOf(str);
		}else if(clazz == String.class) {
			 return (T)str;
		}else if(clazz == long.class || clazz == Long.class) {
			return  (T)Long.valueOf(str);
		}else {
			return JSON.toJavaObject(JSON.parseObject(str), clazz);
		}
	}

	private void returnToPool(Jedis jedis) {
		 if(jedis != null) {
			 jedis.close();
		 }
	}

为了防止多人开发时key值相互覆盖,设置添加一个前缀

【KeyPrefix.java】 接口类

public interface KeyPrefix {
	public int expireSeconds();
	public String getPrefix();
}

【BasePrefix.java】 抽象类

package com.hjx.redis;
public abstract class BasePrefix implements KeyPrefix{
	private int expireSeconds;
	private String prefix;
	
	public BasePrefix(String prefix) {//0代表永不过期
		this(0, prefix);
	}
	
	public BasePrefix( int expireSeconds, String prefix) {
		this.expireSeconds = expireSeconds;
		this.prefix = prefix;
	}
	
	public int expireSeconds() {//默认0代表永不过期
		return expireSeconds;
	}

	public String getPrefix() {
		String className = getClass().getSimpleName();
		return className+":" + prefix;
	}
}

【UserKey.java】 【OrderKey.java】等 实现类

package com.hjx.redis;
public class UserKey extends BasePrefix{
   private UserKey(String prefix) {
      super(prefix);
   }
   public static UserKey getById = new UserKey("id");
   public static UserKey getByName = new UserKey("name");
}

Controller 里写个测试接口

@Controller
public class RedisController {
    @Autowired
    RedisService redisService;

    @RequestMapping("/redis/set")
    @ResponseBody
    public Result<String> redisSet() {
        redisService.set(UserKey.getById, "1","hello-1");
        return Result.success("true");
    }

    @RequestMapping("/redis/get")
    @ResponseBody
    public Result<String> redisGet() {
        String  value  = redisService.get(UserKey.getById, "1", String.class);
        return Result.success(value);
    }

}

登入

两次MD5加密

1、用户端 :PASS = MD5(明文 + 固定Salt)

  • 防止密码数据在网络上明文传输 被人拦截看到

2、服务端:PASS = MD5(用户输入 + 随机Salt)

  • 预防服务器(数据库)被入侵

首先创建数据库

CREATE TABLE `miaosha_user`(
	`id` BIGINT(20) NOT NULL COMMENT '用户ID 手机号',
	`nickname` VARCHAR(255) NOT NULL,
	`password` VARCHAR(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt)+salt)',
	`salt` VARCHAR(10) DEFAULT NULL,
	`head` VARCHAR(120) DEFAULT NULL COMMENT '头像,云存储的ID',
	`regist_date` datetime DEFAULT NULL COMMENT '注册时间',
	`last_login_date` datetime DEFAULT NULL COMMENT '上次登录时间',
	`login_count` INT(11) DEFAULT '0' COMMENT '登入次数',
	PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8mb4

【pom.xml】 引入依赖

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.6</version>
</dependency>

【MD5Util.java】

package com.hjx.util;
import org.apache.commons.codec.digest.DigestUtils;

public class MD5Util {
    public static String md5(String src){
        return DigestUtils.md5Hex(src);
    }
    private static final String salt = "1a2b3c";
	//第一次加密逻辑
    public static String inputPassToFormPass(String inputPass) {
        String str = ""+salt.charAt(0)+salt.charAt(2) + inputPass +salt.charAt(5) + salt.charAt(4);
        System.out.println(str);
        return md5(str);
    }
	//第二次加密逻辑
    public static String formPassToDBPass(String formPass, String salt) {
        String str = ""+salt.charAt(0)+salt.charAt(2) + formPass +salt.charAt(5) + salt.charAt(4);
        return md5(str);
    }
	
    public static String inputPassToDbPass(String inputPass, String saltDB) {
        String formPass = inputPassToFormPass(inputPass);
        String dbPass = formPassToDBPass(formPass, saltDB);
        return dbPass;
    }
}

参数校验

【pom.xml】 引入依赖

 <!--验证-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

validation有很多的已写好的校验器,可以很方便地通过注解的方式使用,极大的减少了无效业务代码

自定义校验器:

package com.hjx.validator;
...
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {
	
	boolean required() default true;
	
	String message() default "手机号码格式错误";

	Class<?>[] groups() default { };

	Class<? extends Payload>[] payload() default { };
}

校验方法实现

package com.hjx.validator;
...
    
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

   private boolean required = false;  //是否为空,false可以为空
   private static final Pattern mobile_pattern = Pattern.compile("1\\d{10}");

   public void initialize(IsMobile constraintAnnotation) {
      required = constraintAnnotation.required();
   }

   public boolean isValid(String value, ConstraintValidatorContext context) {
      if(required) {
         if(StringUtils.isEmpty(value)) {
            return false;
         }
         Matcher m = mobile_pattern.matcher(value);
         return m.matches();
      }else {
         if(StringUtils.isEmpty(value)) {
            return true;
         }else {
            Matcher m = mobile_pattern.matcher(value);
            return m.matches();
         }
      }
   }
}

建一个【vo】包,写一个【LoginVo.java】用来接受登录界面的值 ,在需要校验的字段上添加注解

扩展:深入理解 DAO,DTO,DO,VO,AO,BO,POJO,PO,Entity,Model,View的概念

package com.hjx.vo;

import javax.validation.constraints.NotNull;
import com.hjx.validator.IsMobile;
import org.hibernate.validator.constraints.Length;

public class LoginVo {
   
   @NotNull
   @IsMobile
   private String mobile;
   
   @NotNull
   @Length(min=32)
   private String password;
   
   public String getMobile() {
      return mobile;
   }
   public void setMobile(String mobile) {
      this.mobile = mobile;
   }
   public String getPassword() {
      return password;
   }
   public void setPassword(String password) {
      this.password = password;
   }
   @Override
   public String toString() {
      return "LoginVo [mobile=" + mobile + ", password=" + password + "]";
   }
}

登入接口

@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin(@Valid LoginVo loginVo) {
    log.info(loginVo.toString());
    //登录
    CodeMsg loginCodeMsg = miaoshaUserService.login(loginVo);
    if(loginCodeMsg.getCode()!=0){
        return Result.error(loginCodeMsg);
    }
    return Result.success("成功");
}

service

 public CodeMsg login(LoginVal loginVal){
     if(null==loginVal){
         throw new GlobalException(CodeMsg.SERVER_ERROR);
     }
     String mobile=loginVal.getMobile();
     String password=loginVal.getPassword();
     MiaoshaUser user=miaoShaUserDao.getUserById(Long.parseLong(mobile));
     if(null==user){
         return CodeMsg.MSG_MOBILE_NOT_EXIST;
     }
     //
     if(!user.getPassword().equals(MD5Util.formPassword2DbPass(password,user.getSalt())) ){
         return CodeMsg.MSG_PASSWORD_ERROR;
     }

     return CodeMsg.SUCCESS;
 }

全局异常拦截器

[springboot之全局异常拦截器](https://blog.csdn.net/qq_36922927/article/details/8202668

【GlobalException.java】自定义一个全局异常:继承自RuntimeException

package com.hjx.exception;
import com.hjx.result.CodeMsg;
public class GlobalException extends RuntimeException{
   private static final long serialVersionUID = 1L;
   private CodeMsg cm;
 
   public GlobalException(CodeMsg cm) {
      super(cm.toString());
      this.cm = cm;
   }
   public CodeMsg getCm() {
      return cm;
   }
}

【GlobalExceptionHandler.java】GlobalException全局异常拦截器

拦截所有异常 返回错误消息

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {
   @ExceptionHandler(value=Exception.class)  //拦截所有的异常
   public Result<String> exceptionHandler(HttpServletRequest request, Exception e){
      e.printStackTrace();  //打印异常
      if(e instanceof GlobalException) {  //如果异常是自定义的全局异常 返回codeMsg
         GlobalException ex = (GlobalException)e;
         return Result.error(ex.getCm());
      }else if(e instanceof BindException) {  //如果异常是BindException
         BindException ex = (BindException)e;
         List<ObjectError> errors = ex.getAllErrors();
         ObjectError error = errors.get(0);
         String msg = error.getDefaultMessage();
         return Result.error(CodeMsg.BIND_ERROR.fillArgs(msg));//将具体错误信息设置到CodeMsg中返回

      }else {
         return Result.error(CodeMsg.SERVER_ERROR);
      }
   }
}

【CodeMsg.java】 写一个传参数的方法

...
public static CodeMsg BIND_ERROR = new CodeMsg(500101, "参数校验异常:%s");
...
public CodeMsg fillArgs(Object... args) {
    int code = this.code;
    String message = String.format(this.msg, args);
    return new CodeMsg(code, message);
}

使用异常拦截器后的controller(没有业务逻辑代码了)和 service

@RequestMapping("/do_login")
@ResponseBody
public Result<Boolean> doLogin( @Valid LoginVo loginVo) {
    log.info(loginVo.toString());
    miaoshaUserService.login(loginVo);
    return Result.success(true); // 有错误service直接抛异常,异常拦截器再返回错误消息,所以这里只用返回true的消息
}
 public boolean login(LoginVal loginVal){
     if(null==loginVal){
         throw new GlobalException(CodeMsg.SERVER_ERROR);
     }
     String mobile=loginVal.getMobile();
     String password=loginVal.getPassword();
     MiaoshaUser user=miaoShaUserDao.getUserById(Long.parseLong(mobile));
     if(null==user){
         throw new GlobalException( CodeMsg.MSG_MOBILE_NOT_EXIST);
     }
     if(!user.getPassword().equals(MD5Util.formPassword2DbPass(password,user.getSalt())) ){
         throw  new GlobalException(CodeMsg.MSG_PASSWORD_ERROR);
     }
     return true;
 }

分布式session

简单说就是使用第三方缓存(Redis) 来保存一个token,登陆后将token保存到客户端cookies里,每次请求都带上,达到服务器自带的session效果,还可以让存在在不同的服务器的程序请求到reids里的token,来验证登录状态。

WebMvcConfigurerAdapter详解

springboot之参数解析器(WebMvcConfigurerAdapter)

【MiaoshaUserService.java】 登入成功后 给返回的response 添加一个cookie,cookie里保存token信息

public boolean login(HttpServletResponse response, LoginVo loginVo) {
    if(loginVo == null) {
        throw new GlobalException(CodeMsg.SERVER_ERROR);
    }
    String mobile = loginVo.getMobile();
    String formPass = loginVo.getPassword();
    //判断手机号是否存在
    MiaoshaUser user = getById(Long.parseLong(mobile));
    if(user == null) {
        throw new GlobalException(CodeMsg.MOBILE_NOT_EXIST);
    }
    //验证密码
    String dbPass = user.getPassword();
    String saltDB = user.getSalt();
    String calcPass = MD5Util.formPassToDBPass(formPass, saltDB);
    if(!calcPass.equals(dbPass)) {
        throw new GlobalException(CodeMsg.PASSWORD_ERROR);
    }
    //生成cookie
    String token = UUIDUtil.uuid();
    addCookie(response, token, user);
    return true;
}
private void addCookie(HttpServletResponse response, String token, MiaoshaUser user) {
    redisService.set(MiaoshaUserKey.token, token, user);
    Cookie cookie = new Cookie(COOKI_NAME_TOKEN, token);
    cookie.setMaxAge(MiaoshaUserKey.token.expireSeconds());
    cookie.setPath("/");
    response.addCookie(cookie);
}
public MiaoshaUser getByToken(HttpServletResponse response, String token) {
    if(StringUtils.isEmpty(token)) {
        return null;
    }
    MiaoshaUser user = redisService.get(MiaoshaUserKey.token, token, MiaoshaUser.class);
    //延长有效期
    if(user != null) {
        addCookie(response, token, user);
    }
    return user;
}

请添加图片描述

【UserArgumentResolver.java】 使用spring的参数解析器

package com.hjx.config;
......
@Service
public class UserArgumentResolver implements HandlerMethodArgumentResolver {

	@Autowired
	MiaoshaUserService miaoshaUserService;
	//判断参数类型是否是支持的,支持则范返回true
	public boolean supportsParameter(MethodParameter parameter) {
		Class<?> clazz = parameter.getParameterType();
		return clazz== MiaoshaUser.class;
	}
	//处理参数,相当于初始化参数
	public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
			NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        // 获取到request、response
		HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
		HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
		//分别获取请求参数Param,cookie中的token
		String paramToken = request.getParameter(MiaoshaUserService.COOKI_NAME_TOKEN);
		String cookieToken = getCookieValue(request, MiaoshaUserService.COOKI_NAME_TOKEN);
        
		if(StringUtils.isEmpty(cookieToken) && StringUtils.isEmpty(paramToken)) {
			return null;
		}
		String token = StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
		return miaoshaUserService.getByToken(response, token);
	}
	//遍历cookies获得需要的cookie
	private String getCookieValue(HttpServletRequest request, String cookiName) {
		Cookie[]  cookies = request.getCookies();
		for(Cookie cookie : cookies) {
			if(cookie.getName().equals(cookiName)) {
				return cookie.getValue();
			}
		}
		return null;
	}
}

未使用参数解析器前的controller

@RequestMapping("/to_list")
public String toList(Model model,
                     @CookieValue(value = ConstUtil.COOKIE_NAME_TOKEN,required = false)String cookieToken,
                     @RequestParam(value = ConstUtil.COOKIE_NAME_TOKEN,required = false)String paramToken,
                     HttpServletResponse response){
        if(StringUtils.isEmpty(cookieToken)&&StringUtils.isEmpty(paramToken)){
            return "to_login";
        }
  String token=StringUtils.isEmpty(paramToken)?cookieToken:paramToken;
   MiaoshaUser user=userService.getUserByToken(token,response);
    model.addAttribute("user",new MiaoshaUser());
    return "goods_list";
}

使用后

@RequestMapping("/to_list")
public String list(Model model, MiaoshaUser user) {
    model.addAttribute("user", user);//这里只是随便显示一下,商品表还没建
    return "goods_list";
}

秒杀系统实现

数据库创建

CREATE TABLE `goods`(
	`id` BIGINT(20) NOT NULL COMMENT '商品ID',
	`goods_name` VARCHAR(16) NOT NULL COMMENT '商品名称',
	`goods_title` VARCHAR(64) DEFAULT NULL COMMENT '商品标题',
	`goods_img` VARCHAR(64) DEFAULT NULL COMMENT '商品图片',
	`goods_detail` LONGTEXT  COMMENT '商品详情介绍',
	`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
	`goods_stock` INT DEFAULT '0' COMMENT '商品库存,-1表示没有限制',
	PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `miaosha_goods`(
	`id` BIGINT(20) NOT NULL COMMENT '秒杀商品ID',
	`goods_id` BIGINT(20) NOT NULL COMMENT '商品ID',
	`miaosha_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '秒杀价',
	`stock_count` INT(11) DEFAULT NULL COMMENT '库存数量',
	`start_date` datetime DEFAULT NULL COMMENT '秒杀开始时间',
	`end_date` datetime DEFAULT NULL COMMENT '秒杀结束时间',
	PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `order_info`(
	`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
	`user_id` BIGINT(20) DEFAULT NULL COMMENT '用户ID',
	`goods_id` BIGINT(20) DEFAULT NULL COMMENT '商品ID',
	`delivery_add_id` BIGINT(20) DEFAULT NULL COMMENT '收货地址ID',
	`goods_name` VARCHAR(16) DEFAULT NULL COMMENT '冗余过来的商品名称',
	`goods_count` INT(11) DEFAULT '0' COMMENT '商品数量',
	`goods_price` DECIMAL(10,2) DEFAULT '0.00' COMMENT '商品单价',
	`order_channel` TINYINT(4) DEFAULT '0' COMMENT '1pc,2android,3ios',
	`status` TINYINT(4) DEFAULT '0' COMMENT '订单状态,0新建未支付,1已支付,2已发货,3已收货,4已退款,已完成',
	`create_date` datetime DEFAULT NULL COMMENT '订单创建时间',
	`pay_date` datetime DEFAULT NULL COMMENT '支付时间',
	PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;

CREATE TABLE `miaosha_order` (
    `id`  bigint(20) NOT NULL AUTO_INCREMENT,
    `user_id`  bigint(20) NULL COMMENT '用户ID' ,
    `order_id`  bigint(20) NULL COMMENT '订单ID' ,
    `goods_id`  bigint(20) NULL COMMENT '商品ID' ,
    PRIMARY KEY (`id`)
)ENGINE=InnoDB DEFAULT CHARACTER SET=utf8mb4 AUTO_INCREMENT=3;


创建bean、mapper、service 略

如何联表查询

首先创建一个用来接收连表查询数据的Bean对象

继承Goods 、属性为MiaoshaGoods的属性

public class GoodsVo extends Goods {
	private Double miaoshaPrice;
	private Integer stockCount;
	private Date startDate;
	private Date endDate;
	public Integer getStockCount() {
		return stockCount;
	}
	public void setStockCount(Integer stockCount) {
		this.stockCount = stockCount;
	}
	public Date getStartDate() {
		return startDate;
	}
	public void setStartDate(Date startDate) {
		this.startDate = startDate;
	}
	public Date getEndDate() {
		return endDate;
	}
	public void setEndDate(Date endDate) {
		this.endDate = endDate;
	}
	public Double getMiaoshaPrice() {
		return miaoshaPrice;
	}
	public void setMiaoshaPrice(Double miaoshaPrice) {
		this.miaoshaPrice = miaoshaPrice;
	}
}

查询语句

@Select("select g.*,mg.stock_count, mg.start_date, mg.end_date,mg.miaosha_price from miaosha_goods mg left join goods g on mg.goods_id = g.id")
public List<GoodsVo> listGoodsVo();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值