从零搭建基于SpringBoot的秒杀系统(八):通过分布式锁解决多线程导致的问题

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 在前面一篇博客中,通过mysql的优化解决了超卖的问题,但是没有解决同一用户有几率多次购买的问题,这个问题可以通过加锁来解决,解决思路在于一个请求想要购买时需要先获得分布式锁,如果得不到锁就等待。

网络异常,图片无法展示
|


在前面一篇博客中,通过mysql的优化解决了超卖的问题,但是没有解决同一用户有几率多次购买的问题,这个问题可以通过加锁来解决,解决思路在于一个请求想要购买时需要先获得分布式锁,如果得不到锁就等待。


(一)使用redis实现分布式锁

在config下新建RedisConfig,用来写redis的通用配置文件:

public class RedisConfig {
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;
    @Bean
    private RedisTemplate<String,Object> redisTemplate(){
        RedisTemplate<String,Object> redisTemplate=new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key、value的默认序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
    @Bean
    public StringRedisTemplate stringRedisTemplate(){
        StringRedisTemplate stringRedisTemplate=new StringRedisTemplate();
        stringRedisTemplate.setConnectionFactory(redisConnectionFactory);
        return stringRedisTemplate;
    }
}

对于redis的入门,可以看我往期的redis教程https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fqq_41973594%2Fcategory_9831988.html


对于redis更加深入的理解,我将在后续博客中发布。

配置完成之后,接下来就可以在代码中直接使用redis,在KillServiceImpl中增加KillItemV3版本,借助redis的原子操作实现分布式锁


@Autowired
private StringRedisTemplate stringRedisTemplate;
//redis分布式锁
public Boolean KillItemV3(Integer killId, Integer userId) throws Exception {
    //借助Redis的原子操作实现分布式锁
    ValueOperations valueOperations=stringRedisTemplate.opsForValue();
    //设置redis的key,key由killid和userid组成
    final String key=new StringBuffer().append(killId).append(userId).toString();
    //设置redis的value
    final String value= String.valueOf(snowFlake.nextId());
    //尝试获取锁
    Boolean result = valueOperations.setIfAbsent(key, value);
    //如果获取到锁才会进行后面的操作
    if (result){
        stringRedisTemplate.expire(key,30, TimeUnit.SECONDS);
        //判断当前用户是否抢购过该商品
        if (itemKillSuccessMapper.countByKillUserId(killId,userId)<=0){
            //获取商品详情
            ItemKill itemKill=itemKillMapper.selectByidV2(killId);
            if (itemKill!=null&&itemKill.getCanKill()==1 && itemKill.getTotal()>0){
                int res=itemKillMapper.updateKillItemV2(killId);
                if (res>0){
                    commonRecordKillSuccessInfo(itemKill,userId);
                    return true;
                }
            }
        }else {
            System.out.println("您已经抢购过该商品");
        }
        //释放锁
        if (value.equals(valueOperations.get(key).toString())){
            stringRedisTemplate.delete(key);
        }
    }
    return false;
}

相比之前的版本,这个版本想要抢购商品需要先获取redis的分布式锁。

在KillService中增加接口代码:

Boolean KillItemV3(Integer killId,Integer userId) throws Exception;

在KillController中增加redis分布式锁版本的代码:

//redis分布式锁版本
@RequestMapping(value = prefix+"/test/execute3",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse testexecute3(@RequestBody @Validated KillDto killDto, BindingResult result, HttpSession httpSession){
    if (result.hasErrors()||killDto.getKillid()<0){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    try {
        Boolean res=killService.KillItemV3(killDto.getKillid(),killDto.getUserid());
        if (!res){
            return new BaseResponse(StatusCode.Fail.getCode(),"商品已经抢购完或您已抢购过该商品");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    BaseResponse baseResponse=new BaseResponse(StatusCode.Success);
    baseResponse.setData("抢购成功");
    return baseResponse;
}

在本地运行redis-server,redis的安装包可以前往官网下载,或者在我的公众号《Java鱼仔》中回复 秒杀系统 获取。运行redis-server


修改Http请求的路径为/kill/test/execute3



运行jmeter,发现没有产生超卖的情况,并且每个用户确实只买到了一件商品。


(二)基于Redisson的分布式锁实现

只用redis实现分布式锁有一个问题,如果不释放锁,这个锁就会一直锁住。解决办法就是给这个锁设置一个时间,并且这个设置时间和设置锁需要是原子操作。可以使用lua脚本保证原子性,但是我们更多地是使用基于Redis的第三方库来实现,该项目用到了Redisson。

在config目录下新建RedissonConfig


@Configuration
public class RedissonConfig {
    @Autowired
    private Environment environment;
    @Bean
    public RedissonClient redissonClient(){
        Config config = new Config();
        config.useSingleServer()
                .setAddress(environment.getProperty("redis.config.host"));
//                .setPassword(environment.getProperty("spring.redis.password"));
        RedissonClient client= Redisson.create(config);
        return client;
    }
}

相关的配置放在application.properties中

#redis
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
redis.config.host=redis://127.0.0.1:6379

在killServiceImpl中新建KillItemV4版本

@Autowired
private RedissonClient redissonClient;
//redisson的分布式锁
@Override
public Boolean KillItemV4(Integer killId, Integer userId) throws Exception {
    Boolean result=false;
    final String key=new StringBuffer().append(killId).append(userId).toString();
    RLock lock=redissonClient.getLock(key);
    //三个参数、等待时间、锁过期时间、时间单位
    Boolean cacheres=lock.tryLock(30,10,TimeUnit.SECONDS);
    if (cacheres){
        //判断当前用户是否抢购过该商品
        if (itemKillSuccessMapper.countByKillUserId(killId,userId)<=0){
            //获取商品详情
            ItemKill itemKill=itemKillMapper.selectByidV2(killId);
            if (itemKill!=null&&itemKill.getCanKill()==1 && itemKill.getTotal()>0){
                int res=itemKillMapper.updateKillItemV2(killId);
                if (res>0){
                    commonRecordKillSuccessInfo(itemKill,userId);
                    result=true;
                }
            }
        }else {
            System.out.println("您已经抢购过该商品");
        }
        lock.unlock();
    }
    return result;
}

与redis的区别在于加锁的过程中设置锁的等待时间和过期时间,在KillService中将KillItemV4的代码加上:

Boolean KillItemV4(Integer killId,Integer userId) throws Exception;

最后在KillController中添加以下接口代码:

//redission分布式锁版本
@RequestMapping(value = prefix+"/test/execute4",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse testexecute4(@RequestBody @Validated KillDto killDto, BindingResult result, HttpSession httpSession){
    if (result.hasErrors()||killDto.getKillid()<0){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    try {
        Boolean res=killService.KillItemV4(killDto.getKillid(),killDto.getUserid());
        if (!res){
            return new BaseResponse(StatusCode.Fail.getCode(),"商品已经抢购完或您已抢购过该商品");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    BaseResponse baseResponse=new BaseResponse(StatusCode.Success);
    baseResponse.setData("抢购成功");
    return baseResponse;
}

运行redis后再次使用jmeter,达到预期不超卖,不多卖的效果。

(三)基于zookeeper的分布式锁

关于zookeeper的内容我会在后续的博客中跟进,这里主要用到zookeeper的进程互斥锁InterProcessMutex,Zookeeper利用path创建临时顺序节点,实现公平锁


// 最常用
public InterProcessMutex(CuratorFramework client, String path){
    // Zookeeper利用path创建临时顺序节点,实现公平锁
    this(client, path, new StandardLockInternalsDriver());
}

首先还是写zookeeper的配置文件,配置文件主要配置Zookeeper的连接地址,命名空间等:

@Configuration
public class ZookeeperConfig {
    @Autowired
    private Environment environment;
    @Bean
    public CuratorFramework curatorFramework(){
        CuratorFramework curatorFramework= CuratorFrameworkFactory.builder()
                .connectString(environment.getProperty("zk.host"))
                .namespace(environment.getProperty("zk.namespace" ))
                .retryPolicy(new RetryNTimes(5,1000))
                .build();
        curatorFramework.start();
        return curatorFramework;
    }
}

application.peoperties配置文件中添加zookeeper相关配置

#zookeeper
zk.host=127.0.0.1:2181
zk.namespace=kill

KillService中添加ItemKillV5版本

Boolean KillItemV5(Integer killId,Integer userId) throws Exception;

接下来在KillServiceImpl中添加Zookeeper分布式锁的相关代码,通过InterProcessMutex按path+killId+userId+"-lock"的路径名加锁,每次调用抢购代码时都需要先acquire尝试获取。超时时间设定为10s

@Autowired
private CuratorFramework curatorFramework;
private final String path="/seckill/zookeeperlock/";
//zookeeper的分布式锁
@Override
public Boolean KillItemV5(Integer killId, Integer userId) throws Exception{
    Boolean result=false;
    InterProcessMutex mutex=new InterProcessMutex(curatorFramework,path+killId+userId+"-lock");
    if (mutex.acquire(10L,TimeUnit.SECONDS)){
        //判断当前用户是否抢购过该商品
        if (itemKillSuccessMapper.countByKillUserId(killId,userId)<=0){
            //获取商品详情
            ItemKill itemKill=itemKillMapper.selectByidV2(killId);
            if (itemKill!=null&&itemKill.getCanKill()==1 && itemKill.getTotal()>0){
                int res=itemKillMapper.updateKillItemV2(killId);
                if (res>0){
                    commonRecordKillSuccessInfo(itemKill,userId);
                    result=true;
                }
            }
        }else {
            System.out.println("您已经抢购过该商品");
        }
        if (mutex!=null){
            mutex.release();
        }
    }
    return result;
}

在KillController中添加zookeeper相关的抢购代码:

//zookeeper分布式锁版本
@RequestMapping(value = prefix+"/test/execute5",method = RequestMethod.POST,consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public BaseResponse testexecute5(@RequestBody @Validated KillDto killDto, BindingResult result, HttpSession httpSession){
    if (result.hasErrors()||killDto.getKillid()<0){
        return new BaseResponse(StatusCode.InvalidParam);
    }
    try {
        Boolean res=killService.KillItemV5(killDto.getKillid(),killDto.getUserid());
        if (!res){
            return new BaseResponse(StatusCode.Fail.getCode(),"商品已经抢购完或您已抢购过该商品");
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
    BaseResponse baseResponse=new BaseResponse(StatusCode.Success);
    baseResponse.setData("抢购成功");
    return baseResponse;
}

zookeeper的安装包可以在官网下载,或者在我的公众号《Java鱼仔》中回复 秒杀系统 获取。在Zookeeper的bin目录下运行zkServer,获取到zookeeper的运行信息:



因为已经配置了redis信息,所以还需要启动redis,接着启动系统,通过jmeter进行压力测试



测试后结果即没有超卖,一个人也只能有一个订单。


(四)总结

我们通过三种方式的分布式锁解决了多线程情况下导致实际情况和业务逻辑不通的情况,到这里为止,这个秒杀系统就算粗略的完成了。当然后续还可以有更多的优化,比如添加购物车等功能模块,页面可以美化,在这里redis只用作了分布式锁的功能,还可以热点抢购数据放入redis中。rabbitmq除了异步外,还具有限流、削峰的作用。

最后放上整体系统的代码:

https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FOliverLiy%2FSecondKill


这个系列博客中所有的工具均可在公众号《Java鱼仔》中回复 秒杀系统 获取。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的服装商城管理系统
基于Java+Springboot+Vue开发的服装商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的服装商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
28 2
基于Java+Springboot+Vue开发的服装商城管理系统
|
3天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
基于Java+Springboot+Vue开发的大学竞赛报名管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的大学竞赛报名管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
12 3
基于Java+Springboot+Vue开发的大学竞赛报名管理系统
|
4天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的蛋糕商城管理系统
基于Java+Springboot+Vue开发的蛋糕商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的蛋糕商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
18 3
基于Java+Springboot+Vue开发的蛋糕商城管理系统
|
4天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的美容预约管理系统
基于Java+Springboot+Vue开发的美容预约管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的美容预约管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
19 3
基于Java+Springboot+Vue开发的美容预约管理系统
|
4天前
|
JavaScript Java 关系型数据库
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
本文介绍了一个基于Spring Boot和Vue.js实现的在线考试系统。随着在线教育的发展,在线考试系统的重要性日益凸显。该系统不仅能提高教学效率,减轻教师负担,还为学生提供了灵活便捷的考试方式。技术栈包括Spring Boot、Vue.js、Element-UI等,支持多种角色登录,具备考试管理、题库管理、成绩查询等功能。系统采用前后端分离架构,具备高性能和扩展性,未来可进一步优化并引入AI技术提升智能化水平。
毕设项目&课程设计&毕设项目:基于springboot+vue实现的在线考试系统(含教程&源码&数据库数据)
|
6天前
|
Java 关系型数据库 MySQL
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
本文介绍了一款基于Spring Boot和JSP技术的房屋租赁系统,旨在通过自动化和信息化手段提升房屋管理效率,优化租户体验。系统采用JDK 1.8、Maven 3.6、MySQL 8.0、JSP、Layui和Spring Boot 2.0等技术栈,实现了高效的房源管理和便捷的租户服务。通过该系统,房东可以轻松管理房源,租户可以快速找到合适的住所,双方都能享受数字化带来的便利。未来,系统将持续优化升级,提供更多完善的服务。
毕设项目&课程设计&毕设项目:springboot+jsp实现的房屋租租赁系统(含教程&源码&数据库数据)
|
6天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的房产销售管理系统
基于Java+Springboot+Vue开发的房产销售管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的房产销售管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
23 3
基于Java+Springboot+Vue开发的房产销售管理系统
|
6天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的反诈视频宣传系统
基于Java+Springboot+Vue开发的反诈视频宣传系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的反诈视频宣传管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
40 4
基于Java+Springboot+Vue开发的反诈视频宣传系统
|
8天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的健身房管理系统
基于Java+Springboot+Vue开发的健身房管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的健身房管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
38 5
基于Java+Springboot+Vue开发的健身房管理系统
|
6天前
|
前端开发 JavaScript Java
基于Java+Springboot+Vue开发的医院门诊预约挂号系统
基于Java+Springboot+Vue开发的医院门诊预约挂号系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的门诊预约挂号管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。
30 2
基于Java+Springboot+Vue开发的医院门诊预约挂号系统
下一篇
无影云桌面