【例子】Redis分布式事务锁模拟 秒杀、超卖
上一篇文章介绍了Redisson的分布式锁原理。
Redis的分布式锁原理传送门:
这篇文章来验证一下Redisson分布式锁的作用。
1、搭建Redis主从
我这里使用Redis的主从模式。
搭建Redis主从,一主两从:
1、修改config文件
把redis.confg 复制多两份。
一共三份配置文件,分别是 redis6379.conf、redis6380.conf、redis6381.conf。
1、修改master
redis6379.conf 不需要修改,默认端口是 6379
这里我设置了密码:
requirepass redis
pid修改:
pidfile /var/run/redis_6379.pid
可以另外修改一下允许远程连接,把bind注释。
2、修改 slave
修改 redis6380.conf
端口:
port 6380
pid修改:
pidfile /var/run/redis_6380.pid
指明master :
slaveof 127.0.0.1 6379
因为我的redis配置了 密码 ,需要加上
masterauth redis
redis6381.conf 修改同上。
2、启动
[root@VM-8-8-centos src]# ./redis-server /var/www/web/redis-5.0.8/redis6380.conf
16237:C 16 Oct 2020 09:26:22.275 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
16237:C 16 Oct 2020 09:26:22.275 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=16237, just started
16237:C 16 Oct 2020 09:26:22.275 # Configuration loaded
[root@VM-8-8-centos src]# ./redis-server /var/www/web/redis-5.0.8/redis6381.conf
16248:C 16 Oct 2020 09:26:27.793 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
16248:C 16 Oct 2020 09:26:27.793 # Redis version=5.0.8, bits=64, commit=00000000, modified=0, pid=16248, just started
16248:C 16 Oct 2020 09:26:27.793 # Configuration loaded
成功启动:
[root@VM-8-8-centos src]# ps -aux|grep redis
root 6570 0.0 0.7 167336 13784 ? Ssl Oct14 2:15 ./redis-server *:6379
root 16238 0.0 0.3 153900 6400 ? Ssl 09:26 0:00 ./redis-server *:6380
root 16249 0.0 0.4 153900 7700 ? Rsl 09:26 0:00 ./redis-server *:6381
root 16264 0.0 0.0 112712 956 pts/0 R+ 09:26 0:00 grep --color=auto redis
查看一下配置:
slave 6381:
[root@VM-8-8-centos src]# ./redis-cli -p 6381 -a redis
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:1602812509
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:9c3ed5d61281a184e63c10483a8aeb31c3c57402
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
master 6379 :
[root@VM-8-8-centos src]# ./redis-cli -p 6379 -a redis
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:0
master_replid:d26097e5e79e7475e91e8d0f04b0b756047d2a75
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:0
连接你的redis:
./redis-cli.exe -h 82.71.16.139 -p 6379 -a redis
使用redis-desktop-manager连接:
2、配置Nginx
配置Nginx,分流进入两个服务。
修改nginx.conf
upstream mysite {
server 127.0.0.1:8090 weight=1;
server 127.0.0.1:8091 weight=1;
}
server {
listen 80;
server_name hellocoder.com www.hellocoder.com;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
location / {
proxy_pass http://mysite;
}
}
最后改一下hosts。
127.0.0.1 www.hellocoder.com
127.0.0.1 hellocoder.com
启动nginx。
3、模拟秒杀业务
配置redisson:
引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.7.3</version>
</dependency>
配置Redis:
新建 RedissonConfig.java
:
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// config.useSingleServer();//单机
// config.useMasterSlaveServers();//集群
// config.useSentinelServers();//哨兵
// config.useClusterServers();//集群
// config.setLockWatchdogTimeout(30000);
//使用的Redis主从模式
config.useMasterSlaveServers()
.setPassword("redis")
.setMasterAddress("redis://82.71.16.139:6379")
.addSlaveAddress("redis://82.71.16.139:6380","redis://82.71.16.139:6381");
return Redisson.create(config);
}
新建两个实体:
Book.java
:
/**
* @author HaC
* @date 2020/10/16
* @Description
*/
@Builder
@Data
@TableName("t_book")
@AllArgsConstructor
@NoArgsConstructor
public class Book {
@TableId(value = "book_id", type = IdType.AUTO)
private long bookId;
private String name;
private int count;
}
Order.java
@Builder
@Data
@TableName("t_book_order")
@AllArgsConstructor
@NoArgsConstructor
public class Order {
@TableId(value = "id", type = IdType.AUTO)
private int id;
private String orderId;
private long bookId;
private int status;
private long userId;
private int count;
private String billTime;
}
OrderController.java
:
@RestController
@Slf4j
@RequestMapping("Order/")
public class OrderController {
@Autowired
BookOrderService bookOrderService;
@RequestMapping("/seckill")
public RetResult seckill(@RequestParam(value = "bookId") Long bookId, @RequestParam(value = "userId", required = false) Long userId) {
if (userId == null) {
//模拟userId,随机生成,这里应该有前端传入
userId = (long) (Math.random() * 1000);
}
String result = bookOrderService.seckill(bookId, userId);
return RetResponse.makeOKRsp(result);
}
}
这里模拟了两种情况:
一种是不加锁,第二种是加redis锁
BookOrderService.java
@Slf4j
@Service
public class BookOrderService {
@Autowired
BookMapper bookMapper;
@Autowired
OrderMapper orderMapper;
@Autowired
RedissonClient redissonClient;
public String seckill(Long bookId, Long userId) {
return notLockDemo(bookId, userId);
// return lockDemo(bookId, userId);
}
String lockDemo(Long bookId, Long userId) {
final String lockKey = bookId + ":" + "seckill" + ":RedissonLock";
RLock rLock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待20秒,上锁以后10秒自动解锁
Boolean flag = rLock.tryLock(20, 10, TimeUnit.SECONDS);
if (flag) {
//1、判断这个用户id 是否已经秒杀过
List<Order> list = orderMapper.selectList(new QueryWrapper<Order>().lambda().eq(Order::getUserId, userId).eq(Order::getStatus, 1).eq(Order::getBookId, bookId));
if (list.size() >= 1) {
log.info("你已经抢过了");
return "你已经抢过了,一人只能抢一次";
}
//2、查库存
Book book = bookMapper.selectOne(new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
if (book != null && book.getCount() > 0) {
//生成订单
String orderId = UUID.randomUUID().toString();
Order newOrder = Order.builder().
orderId(orderId).
status(1).
bookId(bookId).
userId(userId).
count(1).
billTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).build();
orderMapper.insert(newOrder);
//更新库存
Book newBook = Book.builder().count(book.getCount() - 1).build();
bookMapper.update(newBook, new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
log.info("userId:{} 秒杀成功", userId);
return "秒杀成功" + "";
} else {
log.info("秒杀失败,被抢完了");
}
} else {
log.info("请勿重复点击,userid:{} ", userId);
return "你已经抢过了";
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (rLock.isLocked()) {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
}
}
}
return "很遗憾,没货了...";
}
String notLockDemo(Long bookId, Long userId) {
//1、判断这个用户id 是否已经秒杀过
List<Order> list = orderMapper.selectList(new QueryWrapper<Order>().lambda().eq(Order::getUserId, userId).eq(Order::getStatus, 1).eq(Order::getBookId, bookId));
if (list.size() >= 1) {
log.info("你已经抢过了");
return "你已经抢过了,一人只能抢一次";
}
//2、查库存
Book book = bookMapper.selectOne(new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
if (book != null && book.getCount() > 0) {
//生成订单
String orderId = UUID.randomUUID().toString();
Order newOrder = Order.builder().
orderId(orderId).
status(1).
bookId(bookId).
userId(userId).
count(1).
billTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).build();
orderMapper.insert(newOrder);
//更新库存
Book newBook = Book.builder().count(book.getCount() - 1).build();
bookMapper.update(newBook, new QueryWrapper<Book>().lambda().eq(Book::getBookId, bookId));
log.info("userId:{} 秒杀成功", userId);
return "秒杀成功" + "";
} else {
log.info("秒杀失败,被抢完了");
return "很遗憾,没货了...";
}
}
}
新建两个表。
t_book、t_book_order
DROP TABLE IF EXISTS `t_book` ;
CREATE TABLE `t_book` (
`book_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`name` varchar(400) DEFAULT NULL COMMENT '名称',
`count` int DEFAULT 0 COMMENT '数量',
PRIMARY KEY (`book_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='商品表';
DROP TABLE IF EXISTS `t_book_order` ;
CREATE TABLE `t_book_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`order_id` varchar(100) NOT NULL COMMENT '订单号',
`book_id` bigint(20) NOT NULL COMMENT '商品id',
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`status` int DEFAULT 1 COMMENT '状态',
`count` int DEFAULT 0 COMMENT '购买数量',
`bill_time` datetime DEFAULT NULL COMMENT '下单时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='订单表';
INSERT INTO `seckill`.`t_book`(`book_id`, `name`, `count`) VALUES (1, '《HaC的自传》', 5);
4、测试
启动服务,启动两个端口的服务,模拟分布式部署。
1、不加锁情况:
使用jmeter 模拟并发。不加锁的情况模拟10个请求在1s发出 共2次,方便查看:
点击start。
查看一下日志,日志显示不止5个订单是成功的。
8090这台服务器:
8091这台服务器:
同一时间进入请求。
查询一下订单:
库存为0之后,但是初始化只有 5 本书,最后竟然出现了18个订单,显然是有问题的。
这就是不加锁的结果。
2、加锁情况:
清空表:
TRUNCATE TABLE t_book_order;
UPDATE t_book SET count = 5 WHERE book_id =1;
放开BookOrderService.java
注释,重启两个服务
public String seckill(Long bookId, Long userId) {
// return notLockDemo(bookId, userId);
return lockDemo(bookId, userId);
}
jmeter设置 1000个请求,共2次
再看一下日志:
8090服务器:
8091服务器:
日志刚好5个订单成功,看一下数据库:
刚好生成 5 个订单,没有超卖的现象。
这就是使用Redisson加锁的结果。
以上就是redisson分布式锁的简单使用,这样我们就可以在代码使用redisson,控制业务的正确性了。
本文图片看不清可以移步 博客 或者 阅读原文查看:https://blog.csdn.net/yudianxiaoxiao/article/details/109213541