前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis分布式事务锁的应用——秒杀、超卖 简单例子 (下)

Redis分布式事务锁的应用——秒杀、超卖 简单例子 (下)

作者头像
HaC
发布2020-12-30 18:00:44
9640
发布2020-12-30 18:00:44
举报
文章被收录于专栏:HaC的技术专栏HaC的技术专栏

上一篇文章介绍了Redisson的分布式锁原理,这篇文章来验证一下Redisson分布式锁的作用。

1、搭建Redis主从

我这里使用Redis的主从模式。

搭建Redis主从,一主两从:

1、修改config文件

把redis.confg 复制多两份。

一共三份配置文件,分别是 redis6379.conf、redis6380.conf、redis6381.conf。

1、修改master

redis6379.conf 不需要修改,默认端口是 6379

这里我设置了密码:

代码语言:javascript
复制
requirepass redis

pid修改:

代码语言:javascript
复制
pidfile /var/run/redis_6379.pid

可以另外修改一下允许远程连接,把bind注释。

2、修改 slave

修改 redis6380.conf

端口:

代码语言:javascript
复制
port 6380

pid修改:

代码语言:javascript
复制
pidfile /var/run/redis_6380.pid

指明master :

代码语言:javascript
复制
slaveof 127.0.0.1 6379

因为我的redis配置了 密码 ,需要加上

代码语言:javascript
复制
masterauth redis

redis6381.conf 修改同上。

2、启动

代码语言:javascript
复制
[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

成功启动:

代码语言:javascript
复制
[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:

代码语言:javascript
复制
[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 :

代码语言:javascript
复制
[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:

代码语言:javascript
复制
./redis-cli.exe -h 82.71.16.139 -p 6379 -a redis

使用redis-desktop-manager连接:

2、配置Nginx

配置Nginx,分流进入两个服务。

修改nginx.conf

代码语言:javascript
复制
	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。

代码语言:javascript
复制
127.0.0.1 www.hellocoder.com
127.0.0.1 hellocoder.com

启动nginx。

3、模拟秒杀业务

配置redisson:

引入依赖:

代码语言:javascript
复制
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.7.3</version>
        </dependency>

配置Redis:

新建 RedissonConfig.java:

代码语言:javascript
复制
@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:

代码语言:javascript
复制
/**
 * @author 公众号:HelloCoder,每天分享Java技术和面试题
 * @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

代码语言:javascript
复制
@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

代码语言:javascript
复制
@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

代码语言:javascript
复制
@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

代码语言:javascript
复制
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

查看一下日志:

8090这台服务器:

8091这台服务器:

同一时间进入请求。

查询一下订单:

库存为0之后,但是初始化只有 5 本书,最后竟然出现了18个订单,显然是有问题的。

这就是不加锁的结果。

2、加锁情况:

清空表:

代码语言:javascript
复制
TRUNCATE TABLE t_book_order;
UPDATE t_book SET count = 5 WHERE book_id =1;

放开BookOrderService.java注释,重启两个服务

代码语言:javascript
复制
    public String seckill(Long bookId, Long userId) {
//        return notLockDemo(bookId, userId);
        return lockDemo(bookId, userId);
    }

jmeter设置 1000个请求,共2次

再看一下日志:

8090服务器:

8091服务器:

看一下数据库:

刚好生成 5 个订单,没有超卖的现象。

以上就是redisson分布式锁的简单使用。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-10-22 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1、搭建Redis主从
  • 2、配置Nginx
  • 3、模拟秒杀业务
  • 4、测试
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档