1、技术栈
后端: SpringBoot + Redis
前端: Bootstrap + Jquery
2、测试环境
IDEA + Maven+ Tomcat8.5 + JDK8
3、下载redis
Redis下载地址
4、基本流程图
Spring的声明式事务通过:传播行为、隔离级别、只读提示、事务超时、回滚规则来进行定义。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- alibaba的druid数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<!-- redis客户端 小白用的是服务器上的-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
配置application.yml文件
server:
port: 8080
spring:
datasource:
name: springboot
type: com.alibaba.druid.pool.DruidDataSource
#druid相关配置
druid:
#监控统计拦截的filters
filter: stat
#mysql驱动
driver-class-name: com.mysql.jdbc.Driver
#基本属性
url: jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&?zeroDateTimeBehavior=convertToNull
username: root
password: sasa
#配置初始化大小/最小/最大
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 60000
#间隔多久进行一次检测,检测需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
thymeleaf:
prefix: classpath:/templates/
check-template-location: true
suffix: .html
encoding: UTF-8
mode: LEGACYHTML5
cache: false
#文件上传相关设置
servlet:
multipart:
max-file-size: 10Mb
max-request-size: 100Mb
#devtools插件
devtools:
livereload:
enabled: true #是否支持livereload
port: 35729
restart:
enabled: true #是否支持热部署
#redis缓存
redis:
#redis数据库索引,默认是0
database: 0
#redis服务器地址
host: 39.105.174.56
# Redis服务器连接密码(默认为空)
password:
#redis服务器连接端口,默认是6379
port: 6379
# 连接超时时间(毫秒)
timeout: 1000
jedis:
pool:
# 连接池最大连接数(使用负值表示没有限制)
max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 8
# 连接池中的最小空闲连接
min-idle: 0
#mybatis配置
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: cn.tycoding.entity
configuration:
# 使用jdbc的getGeneratedKeys 可以获取数据库自增主键值
use-generated-keys: true
# 使用列别名替换列名,默认true。
use-column-label: true
# 开启驼峰命名转换
map-underscore-to-camel-case: true
# 打印sql
logging:
level:
cn.tycoding.mapper: DEBUG
创建数据库seckill.sql文件
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for seckill
-- ----------------------------
DROP TABLE IF EXISTS `seckill`;
CREATE TABLE `seckill` (
`seckill_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
`title` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品标题',
`image` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品图片',
`price` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品原价格',
`cost_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '商品秒杀价格',
`stock_count` bigint(20) NULL DEFAULT NULL COMMENT '剩余库存数量',
`start_time` timestamp(0) NOT NULL DEFAULT '1970-02-01 00:00:01' COMMENT '秒杀开始时间',
`end_time` timestamp(0) NOT NULL DEFAULT '1970-02-01 00:00:01' COMMENT '秒杀结束时间',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`seckill_id`) USING BTREE,
INDEX `idx_start_time`(`start_time`) USING BTREE,
INDEX `idx_end_time`(`end_time`) USING BTREE,
INDEX `idx_create_time`(`end_time`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '秒杀商品表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of seckill
-- ----------------------------
INSERT INTO `seckill` VALUES (1, 'Apple/苹果 iPhone 6s Plus 国行原装苹果6sp 5.5寸全网通4G手机', 'https://g-search3.alicdn.com/img/bao/uploaded/i4/i3/2249262840/O1CN011WqlHkrSuPEiHxd_!!2249262840.jpg_230x230.jpg', 2600.00, 1100.00, 9, '2019-12-22 16:30:00', '2019-12-22 23:30:00', '2019-12-22 21:12:46');
INSERT INTO `seckill` VALUES (2, 'ins新款连帽毛领棉袄宽松棉衣女冬外套学生棉服', 'https://gw.alicdn.com/bao/uploaded/i3/2007932029/TB1vdlyaVzqK1RjSZFzXXXjrpXa_!!0-item_pic.jpg_180x180xz.jpg', 200.00, 150.00, 10, '2019-12-22 16:30:00', '2019-12-22 23:30:00', '2019-12-22 21:12:46');
INSERT INTO `seckill` VALUES (3, '可爱超萌兔子毛绒玩具垂耳兔公仔布娃娃睡觉抱女孩玩偶大号女生 ', 'https://g-search3.alicdn.com/img/bao/uploaded/i4/i2/3828650009/TB22CvKkeOSBuNjy0FdXXbDnVXa_!!3828650009.jpg_230x230.jpg', 160.00, 130.00, 20, '2019-12-22 16:30:00', '2019-12-22 23:30:00', '2019-12-22 21:12:46');
-- ----------------------------
-- Table structure for seckill_order
-- ----------------------------
DROP TABLE IF EXISTS `seckill_order`;
CREATE TABLE `seckill_order` (
`seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品ID',
`money` decimal(10, 2) NULL DEFAULT NULL COMMENT '支付金额',
`user_phone` bigint(20) NOT NULL COMMENT '用户手机号',
`create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',
`state` tinyint(4) NOT NULL DEFAULT -1 COMMENT '状态:-1无效 0成功 1已付款',
PRIMARY KEY (`seckill_id`, `user_phone`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '秒杀订单表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of seckill_order
-- ----------------------------
INSERT INTO `seckill_order` VALUES (1, 1100.00, 15173117830, '2019-12-22 22:00:40', -1);
SET FOREIGN_KEY_CHECKS = 1;
整个项目结构
隔离级别
声明式事务的第二个维度就是隔离级别。隔离级别定义了一个事务可能受其他并发事务影响的程度。多个事务并发运行,经常会操作相同的数据来完成各自的任务,但是可以回导致以下问题:
更新丢失:当多个事务选择同一行操作,并且都是基于最初的选定的值,由于每个事务都不知道其他事务的存在,就会发生更新覆盖的问题。
脏读:事务A读取了事务B已经修改但为提交的数据。若事务B回滚数据,事务A的数据存在不一致的问题。
不可重复读:书屋A第一次读取最初数据,第二次读取事务B已经提交的修改或删除的数据。导致两次数据读取不一致。不符合事务的隔离性。
幻读:事务A根据相同条件第二次查询到的事务B提交的新增数据,两次数据结果不一致,不符合事务的隔离性。
理想情况下,事务之间是完全隔离的,从而可以防止这些问题的发生。但是完全的隔离会导致性能问题,因为它通常会涉及锁定数据库中的记录。侵占性的锁定会阻碍并发性,要求事务互相等待以完成各自的工作。
因此为了实现在事务隔离上有一定的灵活性。因此,就会有多重隔离级别:
隔离级别 | 含义 |
---|---|
ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 |
SIOLATION_READ_UNCOMMITTED | 允许读取尚未提交的数据变更。可能会导致脏读、幻读或不可重复读 |
ISOLATION_READ_COMMITTED | 允许读取并发事务提交的数据。可以阻止脏读,但是幻读或不可重复读仍可能发生 |
ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果是一致的,除非数据是被本事务自己所修改,可以阻止脏读和不可重复读,但幻读仍可能发生 |
ISOLATION_SERIALIZABLE | 完全服从ACID的事务隔离级别,确保阻止脏读、不可重复读、幻读。这是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库来实现的 |
回滚规则
pring的事务管理器默认是针对unchecked exception回滚,也就是默认对Error异常和RuntimeException异常以及其子类进行事务回滚。
也就是说事务只有在遇到运行期异常才会回滚,而在遇到检查型异常时不会回滚。
这也就是我们之前设计Service业务层逻辑的时候一再强调捕获try catch异常,且将编译期异常转换为运行期异常。
配置JedisConfig序列化
package cn.tycoding.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
@Configuration
public class JedisConfig {
private Logger logger = LoggerFactory.getLogger(JedisConfig.class);
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-active}")
private int maxActive;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private int minIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Bean
public JedisPool redisPoolFactory(){
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
jedisPoolConfig.setMaxTotal(maxActive);
jedisPoolConfig.setMinIdle(minIdle);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, null);
logger.info("JedisPool注入成功");
logger.info("redis地址:" + host + ":" + port);
return jedisPool;
}
}
这里是为了将我们在application.yml中配置的参数注入到JedisPool中,使用Spring的@Value注解能读取到Spring配置文件中已经配置的参数的值
package cn.tycoding.redis;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.serializer.Jackson2JsonRedisSerializer;
@Configuration
public class RedisTemplateConfig {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashKeySerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
logger.info("RedisTemplate序列化配置,转化方式:" + jackson2JsonRedisSerializer.getClass().getName());
return redisTemplate;
}
}
注意
实现序列化目前而言不是必须的,因为我们使用了Spring-data-redis提供的高度封装的RedisTemplate模板类。
SpringBoot2.x实现Redis的序列化仍是由很多方案,但是我这里使用了Spring-data-redis提供的一种jackson2JsonRedisSerializer的序列化方式。
如果不实现Redis的序列化,可以往Redis中存入数据,但是存入的key都是乱码的,想要避免这一点就必须实现序列化。
这个步骤和我们之前整合SSM+Redis+Shiro+Solr框架中已经讲到了用XML实现序列化配置,这里仅是换成了Java配置而已。
测试代码的页面效果