把优惠券系统设计的炉火纯青!(2)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: 公司新来一个同事,把优惠券系统设计的炉火纯青!

解决方案一(Java代码加锁)

在引起超发原因的那张图内可以看出,导致这一问题的根本原因是多个线程同时访问这个领取优惠券的方法,那只要保证在同一段只有一个线程进入到这个方法就可以了。

上面贴的代码就可以改成下面这样:

synchronized (this){
    LoginUser loginUser = LoginInterceptor.threadLocal.get();
    CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>()
                                    .eq("id", couponId)
                                    .eq("category", categoryEnum.name()));
    if(couponDO == null){
        throw new BizException(BizCodeEnum.COUPON_NO_EXITS);
    }
    this.checkCoupon(couponDO,loginUser.getId());
    //构建领券记录
    CouponRecordDO couponRecordDO = new CouponRecordDO();
    BeanUtils.copyProperties(couponDO,couponRecordDO);
    couponRecordDO.setCreateTime(new Date());
    couponRecordDO.setUseState(CouponStateEnum.NEW.name());
    couponRecordDO.setUserId(loginUser.getId());
    couponRecordDO.setUserName(loginUser.getName());
    couponRecordDO.setCouponId(couponDO.getId());
    couponRecordDO.setId(null);
    int row = couponMapper.reduceStock(couponId);
    if(row == 1){
        couponRecordMapper.insert(couponRecordDO);
    }else{
        log.info("发送优惠券失败:{},用户:{}",couponDO,loginUser);
    }
}

image.png


这样,经过Jmeter的压测优惠券并没有出现超发的情况。


虽然这样可以解决超发的问题,但是在项目中我们不可以这样写,原因如下:


synchronized的作用范围是单个JVM实例,如果是集群部署系统这里的加锁你可以理解成失效

在使用了synchronized加锁后,就会形成串行等待的问题,当一个线程A在领取优惠券方法内执行过久时,其它线程会等待直到线程A执行结束



解决方案二(Sql层面解决超发)

<update id="reduceStock">
     update coupon set stock = stock - 1 where id = #{coupon_id} and stock > 0
</update>


Mysql默认使用的是InnoDB引擎,使用InnoDB时在修改某一个记录的时候会将这条记录上锁,所以这个修改数据时不会出现多个线程同时修改数据。这样也可以避免优惠券超发。


如果在业务中只要有库存就可以发放优惠券的可以使用上面这种方式。


还有一种Sql的方式,可以将stock自身做为乐观锁。


    update product set stock=stock-1 where stock=#{上一次的库存}  and id = 1 and stock>0



上面这种方式会存在ABA的问题,当然如果业务不在意ABA问题可以使用上面的sql,不过性能可能差一点,如果stock不匹配,这条sql也就失效了。


如果业务在意ABA问题的话也可以在表中加一个version的字段,每次修改数据的时候这个字段会加1,这样就可以避免ABA问题


<update id="reduceStock">
     update product set stock=stock-1,versioin = version+1 where  id = 1 and stock>0 and version=#{上一次的版本号}
</update>



上面的这三条Sql层面的代码都可以解决优惠券超发的问题,具体使用那种就根据业务来选择了


解决方案三(通过Redis分布式锁来解决问题)

引入Redis后,当领取优惠券时会先去Redis里面去获取锁,当锁获取成功后才可以对数据库进行操作


image.png


在分布式锁中我们应该考滤如下:


排他性,在分布式集群中,同一个方法,在同一个时间只能被某一台机器上的一个线程执行

容错性,当一个线程上锁后,如果机器突然的宕机,如果不释放锁,此时这条数据将会被锁死

还要注意锁的粒度,锁的开销

满足高可用,高性能,可重入

我们可以使用Redis里面的setnx命令来设置锁,因为setnx是原子性的操作不可被打断


image.png


当这个命令执行成功的时候会返回1,执行失败会返回0,我们就可以通过这个特性来判断是否获取到了锁。


先看下伪代码:


String key = "lock:coupon:" + couponId;
try{
    if(setnx(key,"1")){
        //获取到锁
        //设置Key的时期时间
        exp(key,30,TimeUnit.MILLISECONDS);
        try{
            //业务逻辑
        }finally{
            del(key);
        }
    }else{
        //获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁
    }
}



这方法里面设置key的过期时间的原因是,当机器突然的宕机后,即使没有释放掉锁,他也会在一段时间后将这个锁释放,避免导致死锁。


虽然看上面的代码是没有问题的,但是它是存在一个误删除key的问题


image.png


为了避免这个问题,可以将setnx命令设置的那个值,设置成当前线程的ID,在删除的时候判断这个线程ID是不是与当前线程的Id相同就可以了。


String key = "lock:coupon:" + couponId;
String threadId = Thread.currentThread().getId();
try{
    if(setnx(key,threadId)){
        //获取到锁
        //设置Key的时期时间
        exp(key,30,TimeUnit.MILLISECONDS);
        try{
            //业务逻辑
        }finally{
            if(get(key) == threadId){
                del(key);
            }
        }
    }else{
        //获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁
    }
}




通过上面这种方法就可以解决误删除key的问题。


在finally中的这个判断和删除key的代码不是原子性的,我们可以通过lua脚本的方式来实现它们之间的原子性,将删除key的代码修改成如下:


String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";


redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), threadId);



这里的threadId其实也可以不用,写成uuid也可以,但是在上面setnx的时候,那个值也要写成uuid


但是这样还要存在一个锁自动续期的问题,你可以开一个守护线程,每隔多久给他续期一次,或者是直接将这个过期时间延长一些。


在Redis中也有一些官方推荐的分布式锁的方式。我最后是使用的这种方式


解决方案四(使用Redis推荐的方式)

官网地址:


https://redis.io/docs/manual/patterns/distributed-locks/


这个有多种实现方式,比如:Golang,Java,Php


引入Redisson包


<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.17.4</version>
</dependency>



配置RedissoneClient


@Configuration
public class AppConfig {
    @Value("${spring.redis.host}")
    private String redisHost;
    @Value("${spring.redis.port}")
    private String redisPort;
    @Bean
    public RedissonClient redisson(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);
        return Redisson.create(config);
    }
}



配置好RedissonClient后,通过getLock方法获取到锁对象后,在我们的Service层中就可以通过lock和unlock来进行加锁和释放锁了,这样还是很方便的。


public JsonData addCoupon(long couponId, CouponCategoryEnum categoryEnum) {
    String key = "lock:coupon:" + couponId;
    RLock rLock = redisson.getLock(key);
    LoginUser loginUser = LoginInterceptor.threadLocal.get();
    rLock.lock();
    try{
       //业务逻辑
    }finally {
        rLock.unlock();
    }
    return JsonData.buildSuccess();
}



通过这种方法也可以解决优惠券超发的问题 ,这也是Rediss官网推荐的一种方式。


使用这种方式也无需关心key过期时间续期的问题,因为在Redisson一旦加锁成功,就会启动一个watch dog,你可以将它理解成一个守护线程,它默认会每隔30秒检查一下,如果当前客户端还占有这把锁,它会自动对这个锁的过期时间进行延长。


也可以通过下面的方法设置watch dog的检测时间间隔


Config config = new Config();
config.setLockWatchdogTimeout();


如上就是我在解决优惠券超发时的一个思路。



相关文章
|
SQL BI 数据库
达梦(DM) SQL日期操作及分析函数
讲述DM 数据库中如何实现各种日期相关的运算以及如何利用分析函数 lead() over() 进行范围问题的处理
|
4月前
|
机器学习/深度学习 人工智能 自然语言处理
教育领域的AI进展:智能辅导与个性化学习的技术革新与挑战
随着人工智能技术的发展,AI Agent在教育领域的应用日益广泛,特别是在智能辅导与个性化学习方面展现出巨大潜力。通过自然语言处理、机器学习和数据分析等技术,AI可模拟个性化辅导员,根据学生的学习情况提供定制化资源与实时反馈。未来,AI Agent将更注重情感分析与跨学科培养,成为教师的有力助手,推动教育公平与效率提升。然而,数据隐私、个体差异及教育资源不平衡等问题仍需克服,以实现更智能化、全面化的教育生态。
365 10
教育领域的AI进展:智能辅导与个性化学习的技术革新与挑战
|
存储 SQL 分布式计算
ClickHouse 高可用之副本
ClickHouse 使用副本机制增强数据可用性,复制数据到多个节点以备故障转移。仅MergeTree系列引擎支持副本,需使用`Replicated`前缀。副本是表级别,需先创建对应表结构。配置高可用副本需借助Zookeeper协调。在三台机器上部署,每台有三份数据。创建副本表时,需指定Zookeeper路径和唯一副本名称。通过`CREATE TABLE`语句在每个节点创建副本表并插入数据,然后验证数据同步。还可以使用工具如PrettyZoo查看Zookeeper中的副本表元数据。
502 0
|
9月前
|
C# Android开发 iOS开发
2025年全面的.NET跨平台应用框架推荐
2025年全面的.NET跨平台应用框架推荐
350 23
|
11月前
|
人工智能 自然语言处理 前端开发
VideoChat:高效学习新神器!一键解读音视频内容,结合 AI 生成总结内容、思维导图和智能问答
VideoChat 是一款智能音视频内容解读助手,支持批量上传音视频文件并自动转录为文字。通过 AI 技术,它能快速生成内容总结、详细解读和思维导图,并提供智能对话功能,帮助用户更高效地理解和分析音视频内容。
627 6
VideoChat:高效学习新神器!一键解读音视频内容,结合 AI 生成总结内容、思维导图和智能问答
|
11月前
|
前端开发 UED 异构计算
CSS优化技巧
【10月更文挑战第28天】通过应用这些CSS优化技巧,可以提高CSS代码的质量和性能,使页面加载更快、更流畅,同时也更易于维护和扩展,提升整个Web项目的开发效率和用户体验。
177 11
|
8月前
|
机器学习/深度学习 监控 Linux
ollama+openwebui本地部署deepseek 7b
Ollama是一个开源平台,用于本地部署和管理大型语言模型(LLMs),简化了模型的训练、部署与监控过程,并支持多种机器学习框架。用户可以通过简单的命令行操作完成模型的安装与运行,如下载指定模型并启动交互式会话。对于环境配置,Ollama提供了灵活的环境变量设置,以适应不同的服务器需求。结合Open WebUI,一个自托管且功能丰富的Web界面,用户可以更便捷地管理和使用这些大模型,即使在完全离线的环境中也能顺利操作。此外,通过配置特定环境变量,解决了国内访问限制的问题,例如使用镜像站来替代无法直接访问的服务。
|
11月前
|
机器学习/深度学习 人工智能 运维
智能化运维:AI驱动下的IT运维革命###
本文探讨了人工智能(AI)技术在IT运维领域的创新应用,强调其在提升效率、预防故障及优化资源配置中的关键作用,揭示了智能运维的新趋势。 ###
|
SQL 监控 Oracle
Oracle 性能优化之AWR、ASH和ADDM(含报告生成和参数解读)
Oracle 性能优化之AWR、ASH和ADDM(含报告生成和参数解读)
|
存储 监控 调度
【Flink】怎么提交的实时任务,有多少Job Manager?
【4月更文挑战第18天】【Flink】怎么提交的实时任务,有多少Job Manager?