设计与开发
表结构
CREATE TABLE `t_product` (
`id` int(12) NOT NULL AUTO_INCREMENT COMMENT '产品编号',
`product_name` varchar(60) NOT NULL COMMENT '产品名称',
`stock` int(10) NOT NULL COMMENT '库存',
`price` decimal(16,2) NOT NULL COMMENT '单价',
`version` int(10) NOT NULL DEFAULT '0' COMMENT '版本号',
`note` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='产品信息表';
CREATE TABLE `t_purchase_record` (
`id` int(12) NOT NULL AUTO_INCREMENT COMMENT '编号',
`user_id` int(12) NOT NULL COMMENT '用户编号',
`product_id` int(12) NOT NULL COMMENT '产品编号',
`price` decimal(16,2) NOT NULL COMMENT '价格',
`quantity` int(12) NOT NULL COMMENT '数量',
`sum` decimal(12,2) NOT NULL COMMENT '总价',
`purchase_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '购买日期',
`note` varchar(512) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='购买信息表';
- 判断产品表 的产品 有没有足够的库存 支持用户的购买,如果有 则对产品 减库存
- 然后在 将 购买信息 插入到购买记录中,如果库存不足,则返回交易失败。
pom引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依赖Redis的异步客户端lettuce -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入Redis的客户端驱动jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
//连接池,可以不引用
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
//web 必须引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
//mybatis 必须引用
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
//驱动必须引用
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
MyBatis 开发持久层
pojo
@Alias("product")
public class ProductPo implements Serializable {
private static final long serialVersionUID = 3L;
private Long id;
private String productName;
private int stock;
private double price;
private int version;
private String note;
}
@Alias("purchaseRecord")
public class PurchaseRecordPo implements Serializable {
private static final long serialVersionUID = -3L;
private Long id;
private Long userId;
private Long productId;
private double price;
private int quantity;
private double sum;
private Timestamp purchaseTime;
private String note;
}
- 定义 Alias 的别名
mapper文件
ProductMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.chapter15.dao.ProductDao">
<!-- 获取产品 -->
<select id="getProduct" parameterType="long" resultType="product">
select id, product_name as productName,
stock, price, version, note from t_product
where id=#{id}
</select>
<!-- 减库存 -->
<update id="decreaseProduct">
update t_product set stock = stock - #{quantity},
version = version +1
where id = #{id}
</update>
</mapper>
PurchaseRecordMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.springboot.chapter15.dao.PurchaseRecordDao">
<insert id="insertPurchaseRecord" parameterType="purchaseRecord">
insert into t_purchase_record(
user_id, product_id, price, quantity, sum, purchase_date, note)
values(#{userId}, #{productId}, #{price}, #{quantity},
#{sum}, now(), #{note})
</insert>
</mapper>
dao
@Mapper
public interface ProductDao {
// 获取产品
public ProductPo getProduct(Long id);
//减库存,而@Param标明MyBatis参数传递给后台
public int decreaseProduct(@Param("id") Long id, @Param("quantity") int quantity);
// public int decreaseProduct(@Param("id") Long id,
// @Param("quantity") int quantity, @Param("version") int version);
}
@Mapper
public interface PurchaseRecordDao {
public int insertPurchaseRecord(PurchaseRecordPo pr);
}
开发业务层 和 控制层
service
public interface PurchaseService {
/**
* 处理购买业务
* @param userId 用户编号
* @param productId 产品编号
* @param quantity 购买数量
* @return 成功or失败
*/
public boolean purchase(Long userId, Long productId, int quantity);
boolean purchaseRedis(Long userId, Long productId, int quantity);
boolean dealRedisPurchase(List<PurchaseRecordPo> prpList);
}
@Service
public class PurchaseServiceImpl implements PurchaseService {
@Autowired
private ProductDao productDao = null;
@Autowired
private PurchaseRecordDao purchaseRecordDao = null;
@Override
// 启动Spring数据库事务机制
@Transactional
public boolean purchase(Long userId, Long productId, int quantity) {
// 获取产品
ProductPo product = productDao.getProduct(productId);
// 比较库存和购买数量
if (product.getStock() < quantity) {
// 库存不足
return false;
}
// 扣减库存
productDao.decreaseProduct(productId, quantity);
// 初始化购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity);
// 插入购买记录
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
// 初始化购买信息
private PurchaseRecordPo initPurchaseRecord(Long userId, ProductPo product, int quantity) {
PurchaseRecordPo pr = new PurchaseRecordPo();
pr.setNote("购买日志,时间:" + System.currentTimeMillis());
pr.setPrice(product.getPrice());
pr.setProductId(product.getId());
pr.setQuantity(quantity);
double sum = product.getPrice() * quantity;
pr.setSum(sum);
pr.setUserId(userId);
return pr;
}
//第一版完毕
}
controller
// REST风格控制器
@RestController
public class PurchaseController {
@Autowired
PurchaseService purchaseService = null;
// 定义JSP视图
@GetMapping("/test")
public ModelAndView testPage() {
ModelAndView mv = new ModelAndView("test");
return mv;
}
@PostMapping("/purchase")
public Result purchase(Long userId, Long productId, Integer quantity) {
boolean success = purchaseService.purchaseRedis(userId, productId, quantity);
String message = success ? "抢购成功" : "抢购失败";
Result result = new Result(success, message);
return result;
}
// 响应结果
class Result {
private boolean success = false;
private String message = null;
public Result() {
}
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
/**** setter and getter ****/
}
}
配置和测试
jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>购买产品测试</title>
<script type="text/javascript"
src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
</head>
<!--后面需要改写这段JavaScript脚本进行测试-->
<script type="text/javascript">
var params = {
userId : 1,
productId : 1,
quantity : 3
};
// 通过POST请求后端
$.post("./purchase", params, function(result) {
alert(result.message);
});
for (var i = 1; i <= 50000; i++) {
var params = {
userId: 1,
productId: 1,
quantity: 1
};
// 通过POST请求后端,这里的JavaScript会采用异步请求
$.post("./purchase", params, function (result) {
});
}
</script>
<body>
<h1>抢购产品测试</h1>
</body>
</html>
配置
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
########## 数据库配置 ##########
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot_chapter15
spring.datasource.username=root
spring.datasource.password=123456
#spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.tomcat.max-idle=10
spring.datasource.tomcat.max-active=50
spring.datasource.tomcat.max-wait=10000
spring.datasource.tomcat.initial-size=5
# 采用隔离级别为读写提交
spring.datasource.tomcat.default-transaction-isolation=2
########## MyBatis配置 ##########
# 映射文件
mybatis.mapper-locations=classpath:com/springboot/chapter15/mapper/*.xml
# 扫描别名
mybatis.type-aliases-package=com.springboot.chapter15.pojo
main方法
// 定义扫描包
@SpringBootApplication(scanBasePackages = "com.springboot.chapter15")
// 定义扫描MyBatis接口
@MapperScan(annotationClass = Mapper.class, basePackages = "com.springboot.chapter15")
@EnableScheduling
public class Chapter15Application {
public static void main(String[] args) {
SpringApplication.run(Chapter15Application.class, args);
}
}
高并发测试
-
线程1:读取库存为1,可购买
-
线程2:读取库存为1,可购买
-
线程1,扣减库存。此时库存为0
-
线程2:扣减库存。此时库存为 -1 。超发了。
-
线程1:插入交易记录。
-
线程2:插入交易记录,错误,库存已经不足。
-
线程2,此时并不会 感知 线程1 的这个操作。而是按照 原来读取到的1,进行扣减。
-
这样就会 出现 -1 。
悲观锁
- 共享的数据 被 多个线程 所 修改,无法保证 其 执行顺序。
- 如果一个数据库事务 读到 产品后,就将数据 直接锁定,不允许其他线程读写,直到 当前事务完成后,才释放这条数据,则不会出现。超发的问题。
<!-- 获取产品 -->
<select id="getProduct" parameterType="long" resultType="product">
select id, product_name as productName,
stock, price, version, note from t_product
where id=#{id} for update
</select>
- for update ,这样 数据库事务 执行的过程中,就会锁定 查询出来的数据,其他事务将 不能再对其进行读写(其他线程执行这行代码的时候,就会进入等待)。
- 不加锁用28s,加悲观锁 用了 33秒。
- 加入事务2 得到商品信息的锁,那么事务 1,3,n 就必须等待 持有 商品信息的 事务 2,结束后 释放商品信息,才能去抢夺 商品信息,这样就会有大量的线程 被 挂机 和 等待。
- 悲观锁:使用数据库内部的锁,对记录进行加锁。
- 悲观锁:也成 独占锁 或 排他锁
乐观锁
乐观锁设计
-
虽然 悲观锁 可以解决 高并发的超发 现象,但并不是一个高效的方案
-
乐观锁:是一种,不使用 数据库锁 和 不阻塞 线程 并发 的方案
-
非独占锁, 无阻塞锁
-
一个线程 先读取 既有的商品 库存数据,保存起来,(旧值)
-
等到 需要对 共享数据 做修改时,会事先 将 保存的旧值库存 与 当前数据库的 库存进行比较。
-
如果 一致,就认为没有被修改过, 否则就认为 已经被修改过,(当前计算不被信任,不在修改数据)
-
保存 旧值——处理业务——旧值 与 当前数据库存 一致
- ——是 扣减库存
- ——否 不执行逻辑
ABA现象 (先A在B,又A)
-
这个方案 就是 多线程的概念,CAS compare and swap
-
会引发 ABA 问题
-
线程1 读取到 A件,保存为A件
-
线程2 读取到A件,保存为A件
-
线程2 扣减库存C件,剩下B件。(当前数据库为A件,与线程旧值一致,成功)
-
线程1,计算剩余商品的价格(总价),会 按照剩余B件 计算。
-
线程2,取消购买,库存回退A件。
- 此时 线程 1 的结果是错的。
-
线程1 计算商品总价格的时候,当前库存 会被线程 2 所修改。
-
称为 ABA问题
-
线程1 在计算商品 总价格时,
-
当前 库存是一个变化的值,这样就可能出现计算错误。
-
共享值回退,导致了数据的不一致。
-
引入版本号
解决ABA,引入版本号
-
规定:只要操作 过程中 修改共享值,无论 业务正常 回退 还是异常
-
版本号 只增不减
-
线程1 读取版本号为1
-
线程2 读取版本号为 1
-
线程2 扣减库存C件, 剩下B件。 版本为2
-
线程2 取消购买,库存回退为A件。 版本为 3
-
线程1 ,计算商品价格 记录的是 版本为1 ,当前已经为 3 了。所以 取消业务。
<!-- 减库存 -->
<update id="decreaseProduct">
update t_product set stock = stock - #{quantity},
version = version +1
where id = #{id} and version = #{version}
</update>
- and version = #{version} 判断,有没有别的事务已经修改过数据
- 一旦 版本号 修改失败,则什么数据 也不会 触发更新
使用乐观锁,版本号处理
public int decreaseProduct(@Param("id") Long id, @Param("quantity") int quantity, @Param("version") int version);
UPDATE t_product //更新这个表
SET stock = stock - 1, //设置为:剩余数量 -1 ,
version = version + 1 //版本号 +1
WHERE
id = '1' //id 为 1 的值
AND version = 1 //并且 版本号,也是 1 ,才更新。
// 启动Spring数据库事务机制
@Transactional(isolation =Isolation.READ_COMMITTED)
public boolean purchase(Long userId, Long productId, int quantity) {
// 获取产品(线程旧值)
ProductPo product = productDao.getProduct(productId);
// 比较库存和购买数量
if (product.getStock() < quantity) {
// 库存不足
return false;
}
// 获取当前版本号
int version = product.getVersion();
// 扣减库存,同时将当前版本号发送给后台去比较
int result = productDao.decreaseProduct(productId, quantity, version);
// 如果更新数据失败,说明数据在多线程中被其他线程修改,导致失败返回
if (result == 0) {
return false;
}
// 初始化购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity);
// 插入购买记录
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
- 耗时 27s ,5万个请求过去,还有库存。没有超发。
- 因为加入了版本号的判断,大量的请求得到失败的结果。
- 这个失败率比较高。
乐观锁 加入 重入机制
- 一旦更新失败,就重新做 一次,称乐观锁为 可重入的锁
- 其原理:一单发现 版本号被更新,不是结束请求,而是重新做一次流程。直到成功为止
- 会带来另一个问题:造成大量的SQL被执行
- 一个请求需要执行3条SQL,重入需要 3次,那么就要12条sql ,会给数据带来压力
- 为了克服:使用 限制 时间 或 重入次数。压制过多的SQL
使用是时间戳 限制重入
-
一个请求 限制 100ms的生存期
-
100ms 内发生版本号冲突,则重试
@Override // 启动Spring数据库事务机制 @Transactional(isolation = Isolation.READ_COMMITTED) public boolean purchase(Long userId, Long productId, int quantity) { // 当前时间 long start = System.currentTimeMillis(); // 循环尝试直至成功 while (true) { // 循环时间 long end = System.currentTimeMillis(); // 如果循环时间大于100毫秒返回终止循环 if (end - start > 100) { return false; } // 获取产品 ProductPo product = productDao.getProduct(productId); // 获取当前版本号 int version = product.getVersion(); // 比较库存和购买数量 if (product.getStock() < quantity) { // 库存不足 return false; } // 扣减库存,同时将当前版本号发送给后台去比较 int result = productDao.decreaseProduct(productId, quantity, version); // 如果更新数据失败,说明数据在多线程中被其他线程修改, // 导致失败,则通过循环重入尝试购买商品 if (result == 0) { continue; } // 初始化购买记录 PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity); // 插入购买记录 purchaseRecordDao.insertPurchaseRecord(pr); return true; } } // 当前时间 long start = System.currentTimeMillis(); // 循环尝试直至成功 while (true) { // 循环时间 long end = System.currentTimeMillis(); // 如果循环时间大于100毫秒返回终止循环 if (end - start > 100) { return false; } // 导致失败,则通过循环重入尝试购买商品 if (result == 0) { continue; } return true; }
-
按照时间戳 重入 也有一个弊端:系统会随自身的忙碌,而大大减少重入的次数
-
因此有时候也会采用 按照次数重入
按照限定次数 重入的乐观锁
@Override
// 启动Spring数据库事务机制,并将隔离级别设置为读写提交
@Transactional(isolation = Isolation.READ_COMMITTED)
public boolean purchase(Long userId, Long productId, int quantity) {
// 循环尝试直至成功
for (int i = 0; i < 3; i++) {
// 获取产品
ProductPo product = productDao.getProduct(productId);
// 比较库存和购买数量
if (product.getStock() < quantity) {
// 库存不足
return false;
}
// 获取当前版本号
int version = product.getVersion();
// 扣减库存,同时将当前版本号发送给后台去比较
int result = productDao.decreaseProduct(productId, quantity,version);
// 如果更新数据失败,说明数据在多线程中被其他线程修改,
// 导致失败,则通过循环重入尝试购买商品
if (result == 0) {
continue;
}
// 初始化购买记录
PurchaseRecordPo pr = this.initPurchaseRecord(userId, product, quantity);
// 插入购买记录
purchaseRecordDao.insertPurchaseRecord(pr);
return true;
}
return false;
}
- 乐观锁:不使用 数据库锁的机制
- 不会造成线程的阻塞,只是采用多版本号 机制来实现
- 因为版本的冲突造成了 请求失败的概率增加 ——往往需要重入的机制机制。
- 重入又会造成 多执行SQL,可以时间戳 或限制重入次数。
- 或者用 redis
使用redis处理高并发
-
数据库 是 写入磁盘的过程。
-
redis : 写入内存 (是 数据库的 几倍 或 数十倍)
- 其命令方式,运算能力比较薄弱(redis lua命令代替)。
- redis lua 执行中 ,具备 原子性
- 使用 redis 去 替代 数据库作为 响应用户的数据载体
- 要处理 redis 存储的不稳定,还需要 有一定的机制 将redis 存储的数据刷入数据库中
-
设计思路
- 先用 redis响应高并发用户的请求
- 及时的将 数据保存到数据库,启用定时任务去查找redis,将它们保存到数据库中
redis 配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!--不依赖Redis的异步客户端lettuce -->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入Redis的客户端驱动jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
#最小 活跃 最大 最大等待
spring.redis.jedis.pool
.min-idle=5
.max-active=10
.max-idle=10
.max-wait=2000
# port host pwd timeout
spring.redis
.port=6379
.host=192.168.2.198
.password=123456
.timeout=1000
- 自动生成,redistTemplate,StringRedisTemplate
Redis 的 Lua编程
@Autowired
StringRedisTemplate stringRedisTemplate = null;
String purchaseScript =
// 先将产品编号保存到集合中
" redis.call('sadd', KEYS[1], ARGV[2]) \n"
// 购买列表
+ "local productPurchaseList = KEYS[2]..ARGV[2] \n"
// 用户编号
+ "local userId = ARGV[1] \n"
// 产品key
+ "local product = 'product_'..ARGV[2] \n"
// 购买数量
+ "local quantity = tonumber(ARGV[3]) \n"
// 当前库存
+ "local stock = tonumber(redis.call('hget', product, 'stock')) \n"
// 价格
+ "local price = tonumber(redis.call('hget', product, 'price')) \n"
// 购买时间
+ "local purchase_date = ARGV[4] \n"
// 库存不足,返回0
+ "if stock < quantity then return 0 end \n"
// 减库存
+ "stock = stock - quantity \n"
+ "redis.call('hset', product, 'stock', tostring(stock)) \n"
// 计算价格
+ "local sum = price * quantity \n"
// 合并购买记录数据
+ "local purchaseRecord = userId..','..quantity..','"
+ "..sum..','..price..','..purchase_date \n"
// 保存到将购买记录保存到list里
+ "redis.call('rpush', productPurchaseList, purchaseRecord) \n"
// 返回成功
+ "return 1 \n";
// Redis购买记录集合前缀
private static final String PURCHASE_PRODUCT_LIST = "purchase_list_";
// 抢购商品集合
private static final String PRODUCT_SCHEDULE_SET = "product_schedule_set";
// 32位SHA1编码,第一次执行的时候先让Redis进行缓存脚本返回
private String sha1 = null;
@Override
public boolean purchaseRedis(Long userId, Long productId, int quantity) {
// 购买时间
Long purchaseDate = System.currentTimeMillis();
Jedis jedis = null;
try {
// 获取原始连接
jedis = (Jedis) stringRedisTemplate
.getConnectionFactory().getConnection().getNativeConnection();
// 如果没有加载过,则先将脚本加载(缓存)到Redis服务器,让其返回sha1
if (sha1 == null) {
sha1 = jedis.scriptLoad(purchaseScript);
}
// 执行脚本,返回结果
Object res = jedis.evalsha(sha1, 2, PRODUCT_SCHEDULE_SET,
PURCHASE_PRODUCT_LIST, userId + "", productId + "",
quantity + "", purchaseDate + "");
//sha1 代表32位 的 SHA1编码
//2 代表 将前面 两个参数 以键 的形式 传递到 脚本中
//后面两个常量是键 。 lua 脚本中 用 keys[index] 表示。keys[1] 第一个键。
//第二个参数 之后,则都是脚本的参数 Lua argv[index]表示,同样 keys[1] 表示
//
Long result = (Long) res;
return result == 1;
} finally {
// 关闭jedis连接
if (jedis != null && jedis.isConnected()) {
jedis.close();
}
}
}
- 保存购买信息
- 将购买记录 保存到 数据库中
@Override
// 当运行方法启用新的独立事务运行。回滚时,只会回滚 这个方法的内部事务。
@Transactional(propagation = Propagation.REQUIRES_NEW)
public boolean dealRedisPurchase(List<PurchaseRecordPo> prpList) {
for (PurchaseRecordPo prp : prpList) {
purchaseRecordDao.insertPurchaseRecord(prp);
productDao.decreaseProduct(prp.getProductId(), prp.getQuantity());
}
return true;
}
定时任务,把redis中数据保存到数据
-
@EnableScheduling
@Service
public class TaskServiceImpl implements TaskService {
@Autowired
private StringRedisTemplate stringRedisTemplate = null;
@Autowired
private PurchaseService purchaseService = null;
private static final String PRODUCT_SCHEDULE_SET = "product_schedule_set";
private static final String PURCHASE_PRODUCT_LIST = "purchase_list_";
// 每次取出1000条,避免一次取出消耗太多内存
private static final int ONE_TIME_SIZE = 1000;
// @Override
// 每天半夜1点钟开始执行任务
// @Scheduled(cron = "0 0 1 * * ?") 秒 分 时 天 月 星期
// 下面是用于测试的配置,每分钟执行一次任务
@Scheduled(fixedRate = 1000 * 60)
public void purchaseTask() {
System.out.println("定时任务开始......");
Set<String> productIdList
= stringRedisTemplate.opsForSet().members(PRODUCT_SCHEDULE_SET);
List<PurchaseRecordPo> prpList = new ArrayList<>();
for (String productIdStr : productIdList) {
//转换成 Long
Long productId = Long.parseLong(productIdStr);
//常量 + 上 long
String purchaseKey = PURCHASE_PRODUCT_LIST + productId;
//绑定这个 list 操作
BoundListOperations<String, String> ops
= stringRedisTemplate.boundListOps(purchaseKey);
// 计算记录数
long size = stringRedisTemplate.opsForList().size(purchaseKey);
//如果 长度 / 1000 == 0 ,就长度/100 ,否则就 长度 +1
Long times = size % ONE_TIME_SIZE == 0 ?
size / ONE_TIME_SIZE : size / ONE_TIME_SIZE + 1;
for (int i = 0; i < times; i++) {
// 获取至多TIME_SIZE个抢红包信息
List<String> prList = null;
if (i == 0) {
prList = ops.range(i * ONE_TIME_SIZE,
(i + 1) * ONE_TIME_SIZE);
} else {
prList = ops.range(i * ONE_TIME_SIZE + 1,
(i + 1) * ONE_TIME_SIZE);
}
for (String prStr : prList) {
PurchaseRecordPo prp
= this.createPurchaseRecord(productId, prStr);
prpList.add(prp);
}
try {
// 采用该方法采用新建事务的方式,这样不会导致全局事务回滚
purchaseService.dealRedisPurchase(prpList);
} catch (Exception ex) {
ex.printStackTrace();
}
// 清除列表为空,等待重新写入数据
prpList.clear();
}
// 删除购买列表
stringRedisTemplate.delete(purchaseKey);
// 从商品集合中删除商品
stringRedisTemplate.opsForSet()
.remove(PRODUCT_SCHEDULE_SET, productIdStr);
}
System.out.println("定时任务结束......");
}
private PurchaseRecordPo createPurchaseRecord(
Long productId, String prStr) {
String[] arr = prStr.split(",");
Long userId = Long.parseLong(arr[0]);
int quantity = Integer.parseInt(arr[1]);
double sum = Double.valueOf(arr[2]);
double price = Double.valueOf(arr[3]);
Long time = Long.parseLong(arr[4]);
Timestamp purchaseTime = new Timestamp(time);
PurchaseRecordPo pr = new PurchaseRecordPo();
pr.setProductId(productId);
pr.setPurchaseTime(purchaseTime);
pr.setPrice(price);
pr.setQuantity(quantity);
pr.setSum(sum);
pr.setUserId(userId);
pr.setNote("购买日志,时间:" + purchaseTime.getTime());
return pr;
}
}
测试
-
rieds命令 执行 命令
-
hmset product_1 id 1 stock 3000 price 5.00
- redis里面会存在键:product_1
- 有3列:第一列:id 1 。 stock 2997。 price 5:00
-
从性能上来讲,只需要6s的时间,比锁 快了 数倍
-
使用redis 建议使用 独立的Redis 服务器,做好备份,容灾。
// Redis购买记录集合前缀
private static final String PURCHASE_PRODUCT_LIST = "purchase_list_";
// 抢购商品集合
private static final String PRODUCT_SCHEDULE_SET = "product_schedule_set";
-
执行过之后redis,product_schedule_set 为 1 (row) 1 (value)
-
purchase_list_1 为: 1 1,1,5,5,1593581880782
-
脚本的执行返回值为 1
-
每次抢购 product_1 stock会减少
-
purchase_list_1 会增加一行
-
定时任务结束后 product_1 之外的两个 redis 清楚