Redis优惠券秒杀企业实战

目录
  • 一、全局唯一ID
    • 1. 全局ID生成器
    • 2. 全局唯一ID生成策略
    • 3. Redis自增ID策略
  • 二、实现优惠券秒杀下单
    • 1. 添加优惠券
    • 2. 编写添加秒杀券的接口
  • 三、实现秒杀下单
  • 四、超卖问题
    • 1. 加锁方式 - 乐观锁
    • 2. 乐观锁解决超卖问题
    • 3. 小结
  • 五、一人一单问题
    • 1. 加锁分析
    • 2. 事务分析
  • 六、集群模式下并发安全问题

一、全局唯一ID

1. 全局ID生成器

每个店铺都可以发布优惠券:

当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题:

  • id的规律性太明显
  • 受单表数据量的限制

所以tb_voucher_order表的主键不能用自增ID:

create table tb_voucher_order
(
    id          bigint                                        not null comment '主键'
        primary key,
    user_id     bigint unsigned                               not null comment '下单的用户id',
    voucher_id  bigint unsigned                               not null comment '购买的代金券id',
    pay_type    tinyint(1) unsigned default 1                 not null comment '支付方式 1:余额支付;2:支付宝;3:微信',
    status      tinyint(1) unsigned default 1                 not null comment '订单状态,1:未支付;2:已支付;3:已核销;4:已取消;5:退款中;6:已退款',
    create_time timestamp           default CURRENT_TIMESTAMP not null comment '下单时间',
    pay_time    timestamp                                     null comment '支付时间',
    use_time    timestamp                                     null comment '核销时间',
    refund_time timestamp                                     null comment '退款时间',
    update_time timestamp           default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
);

全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:

为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:

D的组成部分:

  • 符号位:1bit,永远为0,表示正数
  • 时间戳:31bit,以秒为单位,可以使用69年
  • 序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

编写全局ID生成器代码:

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳,以2022.1.1为基准计算时间差
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 生成带有业务前缀的redis自增id
     * @param keyPrefix
     * @return
     */
    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        // 加上日期前缀,可以让存更多同一业务类型的数据,并且还能通过日期获取当天的业务数量,一举两得
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        // 用于是数字类型的拼接,所以不能像拼接字符串那样处理,而是通过位运算将高32位存 符号位+时间戳,低32位存 序列号
        return timestamp << COUNT_BITS | count;
    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long second = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println(second);// 1640995200
    }
}

测试全局ID生成器:

@SpringBootTest
class HmDianPingApplicationTests {
	@Resource
    private RedisIdWorker redisIdWorker;

    private ExecutorService executorService = Executors.newFixedThreadPool(500);

	@Test
    void testIdWorker() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(300);

        // 每个线程生成100个id
        Runnable task = () -> {
            for (int i = 0; i < 100; i++) {
                long id = redisIdWorker.nextId("order");
                System.out.println("id = " + id);
            }
            latch.countDown();
        };
        // 300个线程
        long begin = System.currentTimeMillis();
        for (int i = 0; i < 300; i++) {
            executorService.submit(task);
        }
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("time = " + (end - begin));
    }
}

测试结果:

2. 全局唯一ID生成策略

  • UUID(不是递增的)
  • Redis自增
  • 雪花算法(snowflake)
  • 数据库自增(单独建一张表存自增id,分配到分库分表后的表中)

3. Redis自增ID策略

  • 以日期作为前缀的key,方便统计订单量
  • 自增ID的结构:时间戳 + 计数器

二、实现优惠券秒杀下单

1. 添加优惠券

每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:

优惠券表信息:

  • tb_voucher:优惠券的基本信息,优惠金额、使用规则等(tb_voucher表的type字段区分是普通券还是秒杀券)
  • tb_seckill_voucher:优惠券的库存、开始抢购时间,结束抢购时间(秒杀券才需要填写这些信息),同时秒杀券拥有普通券的基本信息(秒杀券表tb_seckill_voucher的主键id绑定的是普通券表tb_voucher的id)
create table tb_voucher
(
    id           bigint unsigned auto_increment comment '主键'
        primary key,
    shop_id      bigint unsigned                               null comment '商铺id',
    title        varchar(255)                                  not null comment '代金券标题',
    sub_title    varchar(255)                                  null comment '副标题',
    rules        varchar(1024)                                 null comment '使用规则',
    pay_value    bigint(10) unsigned                           not null comment '支付金额,单位是分。例如200代表2元',
    actual_value bigint(10)                                    not null comment '抵扣金额,单位是分。例如200代表2元',
    type         tinyint(1) unsigned default 0                 not null comment '0,普通券;1,秒杀券',
    status       tinyint(1) unsigned default 1                 not null comment '1,上架; 2,下架; 3,过期',
    create_time  timestamp           default CURRENT_TIMESTAMP not null comment '创建时间',
    update_time  timestamp           default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
);
create table tb_seckill_voucher
(
    voucher_id  bigint unsigned                     not null comment '关联的优惠券的id'
        primary key,
    stock       int(8)                              not null comment '库存',
    create_time timestamp default CURRENT_TIMESTAMP not null comment '创建时间',
    begin_time  timestamp default CURRENT_TIMESTAMP not null comment '生效时间',
    end_time    timestamp default CURRENT_TIMESTAMP not null comment '失效时间',
    update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
)
    comment '秒杀优惠券表,与优惠券是一对一关系';

2. 编写添加秒杀券的接口

主要代码:

@RestController
@RequestMapping("/voucher")
public class VoucherController {

    @Resource
    private IVoucherService voucherService;

    /**
     * 新增秒杀券
     * @param voucher 优惠券信息,包含秒杀信息
     * @return 优惠券id
     */
    @PostMapping("seckill")
    public Result addSeckillVoucher(@RequestBody Voucher voucher) {
        voucherService.addSeckillVoucher(voucher);
        return Result.ok(voucher.getId());
    }
}
@Service
public class VoucherServiceImpl extends ServiceImpl<VoucherMapper, Voucher> implements IVoucherService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Override
    @Transactional
    public void addSeckillVoucher(Voucher voucher) {
        // 保存优惠券
        save(voucher);
        // 保存秒杀信息
        SeckillVoucher seckillVoucher = new SeckillVoucher();
        seckillVoucher.setVoucherId(voucher.getId());
        seckillVoucher.setStock(voucher.getStock());
        seckillVoucher.setBeginTime(voucher.getBeginTime());
        seckillVoucher.setEndTime(voucher.getEndTime());
        seckillVoucherService.save(seckillVoucher);
    }
}

测试添加:

测试结果:

三、实现秒杀下单

下单时需要判断两点:

  • 秒杀是否开始或结束,如果尚未开始或已经结束则无法下单
  • 库存是否充足,不足则无法下单

主要代码:

@RestController
@RequestMapping("/voucher-order")
public class VoucherOrderController {
    @Resource
    private IVoucherOrderService voucherOrderService;

    @PostMapping("seckill/{id}")
    public Result seckillVoucher(@PathVariable("id") Long voucherId) {
        return voucherOrderService.seckillVoucher(voucherId);
    }
}
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }
        //5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).update();
        if (!success) {
            // 扣减库存失败
            return Result.fail("库存不足!");
        }
        // 6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 6.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        // 6.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        return Result.ok(orderId);

    }
}

简单测试秒杀成功:

扣减库存成功:

四、超卖问题

当有大量请求同时访问时,就会出现超卖问题

超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:

1. 加锁方式 - 乐观锁

乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:

(1)版本号法

(2)CAS法

  • 用库存代替了版本号,可以少加一个字段
  • 扣库存时,与查询时的库存比较,没被修改则可以扣减库存

2. 乐观锁解决超卖问题

乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题:

// 之前的代码
boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).update();

// 乐观锁方式,通过CAS判断前后库存是否一致,解决超卖问题
boolean success = seckillVoucherService.update()
            .setSql("stock= stock -1") // set stock = stock -1
            .eq("voucher_id", voucherId).eq("stock",voucher.getStock()).update(); // where id = ? and stock = ?

又出现新的问题:

  • 假设100个线程同时请求,但通过CAS判断后,只有一个线程能扣减库存成功,其余99个线程全部失败
  • 此时,库存剩余99,但是实际业务可以满足其余99个线程扣减库存
  • 虽然能解决超卖问题,但是设计不合理

所以为了解决失败率高的问题,需要进一步改进:

通过CAS 不再 判断前后库存是否一致,而是判断库存是否大于0

boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0

3. 小结

超卖这样的线程安全问题,解决方案有哪些?
(1)悲观锁:添加同步锁,让线程串行执行

  • 优点:简单粗暴
  • 缺点:性能一般

(2)乐观锁:不加锁,在更新时判断是否有其它线程在修改

  • 优点:性能相对悲观锁好(但是仍然需要同时查数据库,影响性能)
  • 缺点:存在成功率低的问题(可以采用分段锁方式提高成功率)

五、一人一单问题

需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单

在扣减库存之前,加上一人一单的逻辑:

// 5.一人一单逻辑
Long userId = UserHolder.getUser().getId();
  // 5.1.查询订单数量
  int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
  // 5.2.判断是否下过单
  if (count > 0) {
      // 用户已经购买过了
      return Result.fail("用户已经购买过一次!");
  }

此处仍会出现并发问题,当同一用户模拟大量请求同时查询是否下过单时,如果正好都查询出count为0,就会跳过判断继续执行扣减库存的逻辑,此时就会出现一人下多单的问题

解决方法:

  • 由于是判断查询的数据是否存在,而不是像之前判断查询的数据是否修改过
  • 所以这里只能加悲观锁

1. 加锁分析

  • 首先将一人一单之后的逻辑全部加锁,所以将一人一单之后的逻辑抽取出一个方法进行加锁,public Result createVoucherOrder(Long voucherId)
  • 如果直接在方法上加锁,则锁的是this对象,锁的对象粒度过大,就算是不同的人执行都会阻塞住,影响性能,public synchronized Result createVoucherOrder(Long voucherId)
  • 所以将锁的对象改为userId,但是不能直接使用synchronized (userId),因为每次执行Long userId = UserHolder.getUser().getId();虽然值一样,但是对象不同,因此需要这样加锁 synchronized (userId.toString().intern()),intern()表示每次从字符串常量池中获取,这样值相同时,对象也相同
  • 为了防止事务还没提交就释放锁的问题,则不能将锁加在createVoucherOrder方法内部,例如:
@Transactional
public Result createVoucherOrder(Long voucherId) {
	synchronized (userId.toString().intern()) {
		。。。
	}
}

而是需要等事务提交完再释放锁,例如:

synchronized (userId.toString().intern()) {
 	// 获取代理对象(事务)
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

2. 事务分析

由于只有一人一单之后的逻辑涉及到修改数据库,所以只需对该方法加事务
@Transactional
public Result createVoucherOrder(Long voucherId)

由于只对createVoucherOrder方法加了事务,而该方法是在seckillVoucher方法中被调用,seckillVoucher方法又没有加事务,为了防止事务失效,则不能直接在seckillVoucher方法调用createVoucherOrder方法,例如:

@Override
public Result seckillVoucher(Long voucherId) {
	。。。。
	synchronized (userId.toString().intern()) {
        return this.createVoucherOrder(voucherId);
    }
}

而是需要通过代理对象调用createVoucherOrder方法,因为@Transactional事务注解的原理是通过获取代理对象执行目标对象的方法,进行AOP操作,所以需要这样:

@Override
public Result seckillVoucher(Long voucherId) {
	。。。。
	// 获取代理对象(事务)
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
}

并且还要引入依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

还要开启注解暴露出代理对象:

@EnableAspectJAutoProxy(exposeProxy = true)
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }
}

完整VoucherOrderServiceImpl代码:

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;

    @Resource
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(Long voucherId) {
        // 1.查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀尚未开始!");
        }
        // 3.判断秒杀是否已经结束
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            // 尚未开始
            return Result.fail("秒杀已经结束!");
        }
        // 4.判断库存是否充足
        if (voucher.getStock() < 1) {
            // 库存不足
            return Result.fail("库存不足!");
        }

        Long userId = UserHolder.getUser().getId();
        synchronized (userId.toString().intern()) {
            // 获取代理对象(事务)
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        }

    }

    @Transactional
    public Result createVoucherOrder(Long voucherId) {

        // 5.一人一单逻辑
        Long userId = UserHolder.getUser().getId();
        // 5.1.查询订单数量
        int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        // 5.2.判断是否下过单
        if (count > 0) {
            // 用户已经购买过了
            return Result.fail("用户已经购买过一次!");
        }

        // 6,扣减库存
        // 乐观锁方式,通过CAS判断库存是否大于0,解决超卖问题:
        boolean success = seckillVoucherService.update()
                .setSql("stock= stock -1")
                .eq("voucher_id", voucherId).gt("stock",0).update(); // where id = ? and stock > 0

        if (!success) {
            // 扣减库存失败
            return Result.fail("库存不足!");
        }

        // 7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        // 7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        // 7.2.用户id
        voucherOrder.setUserId(userId);
        // 7.3.代金券id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);

        // 8.返回订单id
        return Result.ok(orderId);
    }

}

六、集群模式下并发安全问题

通过加锁可以解决在单机情况下的一人一单安全问题,但是在集群模式下就不行了。

我们将服务启动两份,端口分别为8081和8082:

然后修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡:

修改完后,重新加载nginx配置文件:

现在,用户请求会在这两个节点上负载均衡,再次测试下是否存在线程安全问题:

访问8081端口的线程进入了synchronized中

访问8082端口的线程也进入了synchronized中

最终同一个用户下了2单扣了2个库存,所以在集群模式下,出现了一人多单的问题:

分析:

  • 锁的原理是每个JVM中都有一个Monitor作为锁对象,所以当对象相同时,获取的就是同一把锁
  • 但是不同的JVM中的Monitor不同,所以获取的不是同一把锁
  • 因此集群模式下,加synchronized锁也会出现并发安全问题,需要加分布式锁

到此这篇关于Redis优惠券秒杀企业实战的文章就介绍到这了,更多相关Redis 优惠券秒杀 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Redis锁完美解决高并发秒杀问题

    目录 1 单机环境下的锁 2 分布式情况下使用Redis锁. 3 一台服务宕机,导致无法释放锁 4 给每一把锁加上过期时间 5延长锁的过期时间,解决锁失效 6 使用Redisson简化代码 场景:一家网上商城做商品限量秒杀. 1 单机环境下的锁 将商品的数量存到Redis中.每个用户抢购前都需要到Redis中查询商品数量(代替mysql数据库.不考虑事务),如果商品数量大于0,则证明商品有库存.然后我们在进行库存扣减和接下来的操作.因为多线程并发问题,我们不得不在get()方法内部使用同步代码块

  • redis使用watch秒杀抢购实现思路

    本文实例为大家分享了redis使用watch秒杀抢购的具体代码,供大家参考,具体内容如下 1.使用watch,采用乐观锁 2.不使用悲观锁,因为等待时间非常长,响应慢 3.不使用队列,因为并发量会让队列内存瞬间升高 代码: import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import redis.clients.jedis.Jedis; /** * redis测试抢购 * *

  • php+redis实现商城秒杀功能

    好久没来整理文章了,闲了没事写篇文章记录下php+redis实现商城秒杀功能. 1.安装redis,根据自己的php版本安装对应的redis扩展(此步骤简单的描述一下) 1.1.安装php_igbinary.dll,php_redis.dll扩展此处需要注意你的php版本如图: 1.2.php.ini文件新增extension=php_igbinary.dll;extension=php_redis.dll两处扩展 ok此处已经完成第一步redis环境搭建完成看看phpinfo 2.项目中实际使

  • PHP商品秒杀问题解决方案实例详解【mysql与redis】

    本文实例讲述了PHP商品秒杀问题解决方案.分享给大家供大家参考,具体如下: 引言 假设num是存储在数据库中的字段,保存了被秒杀产品的剩余数量. if($num > 0){ //用户抢购成功,记录用户信息 $num--; } 假设在一个并发量较高的场景,数据库中num的值为1时,可能同时会有多个进程读取到num为1,程序判断符合条件,抢购成功,num减一.这样会导致商品超发的情况,本来只有10件可以抢购的商品,可能会有超过10个人抢到,此时num在抢购完成之后为负值. 解决该问题的方案由很多,可

  • Java使用Redis实现秒杀功能

    秒杀功能 秒杀场景现在已经非常常见了,各种电商平台都有秒杀的产品,接下来我们模拟一个秒杀的项目,最终能够确保高并发下的线程安全.界面比较简单,但是功能基本实现. 界面 点击"秒杀点我"按钮后台就会输出秒杀结果. 第一版 使用Redis缓存数据库,使用一个key-value存储秒杀商品数量,使用set集合存储秒杀成功的用户.我们以商品0101为示例,设置商品的初始数量为200件.不考虑并发问题,实现功能. html.jsp.servlet文件不重要省略. package com.redi

  • 使用Redis实现秒杀功能的简单方法

    1. 怎样预防数据库超售现象 设置数据库事务的隔离级别为Serializable(不可用) Serializable就是让数据库去串行化的去执行事务,一个事务执行完才能去执行下一个事务,效率太慢 在数据表上设置乐观锁字段,例如设置版本号(version) 不同事务在执行更新操作时,需要先判断一下版本号是否已被修改 代码实现乐观锁流程 1.1. 什么表需要设置乐观锁 出现同时修改同一条记录的业务,相应的数据表要设置乐观锁 不会出现同时修改同一记录的数据库,就不需要设置乐观锁 2. 利用Redis防

  • php结合redis实现高并发下的抢购、秒杀功能的实例

    抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个: 1 高并发对数据库产生的压力 2 竞争状态下如何解决库存的正确减少("超卖"问题) 对于第一个问题,已经很容易想到用缓存来处理抢购,避免直接操作数据库,例如使用Redis. 重点在于第二个问题 常规写法: 查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数 <?php $conn=mysql_connect("localho

  • Redis中的String类型及使用Redis解决订单秒杀超卖问题

    本系列将和大家分享Redis分布式缓存,本章主要简单介绍下Redis中的String类型,以及如何使用Redis解决订单秒杀超卖问题. Redis中5种数据结构之String类型:key-value的缓存,支持过期,value不超过512M. Redis是单线程的,比如SetAll & AppendToValue & GetValues & GetAndSetValue & IncrementValue & IncrementValueBy等等,这些看上去像是组合命

  • Redis优惠券秒杀企业实战

    目录 一.全局唯一ID 1. 全局ID生成器 2. 全局唯一ID生成策略 3. Redis自增ID策略 二.实现优惠券秒杀下单 1. 添加优惠券 2. 编写添加秒杀券的接口 三.实现秒杀下单 四.超卖问题 1. 加锁方式 - 乐观锁 2. 乐观锁解决超卖问题 3. 小结 五.一人一单问题 1. 加锁分析 2. 事务分析 六.集群模式下并发安全问题 一.全局唯一ID 1. 全局ID生成器 每个店铺都可以发布优惠券: 当用户抢购时,就会生成订单并保存到tb_voucher_order这张表中,而订单

  • Redis实现短信登录的企业实战

    目录 一.导入黑马点评项目 1. 导入SQL 2. 前后端分离 3. 导入后端项目 4. 导入前端项目 二.基于Session实现登录流程 1. 发送短信验证码 2. 短信验证码登录.注册 3. 登录验证功能 三.集群的session共享问题 四.基于Redis实现共享session的登录功能 1. 选择合适的数据结构存入Redis 2. 发送短信验证码 3. 短信验证码登录.注册 4. 解决token刷新问题 一.导入黑马点评项目 黑马点评项目主要包括以下功能: 这一章主要介绍短信登录功能,短

  • php和redis实现秒杀活动的流程

    1 说明 前段时间面试的时候,一直被问到如何设计一个秒杀活动,但是无奈没有此方面的实际经验,所以只好凭着自己的理解和一些资料去设计这么一个程序 主要利用到了redis的string和set,string主要是利用它的k-v结构去对库存进行处理,也可以用list的数据结构来处理商品的库存,set则用来确保用户进行重复的提交 其中我们最主要解决的问题是 -防止并发产生超抢/超卖 2 流程设计 3 代码 3.1 服务端代码 class MiaoSha{ const MSG_REPEAT_USER =

  • Redis中秒杀场景下超时与超卖问题的解决方案

    目录 超时 1.redis连接超时原因 2.解决方法 超卖 1.秒杀超卖现象 2.解决方案 (1)利用乐观锁淘汰用户,解决超卖问题 (2).使用reids的 watch + multi + setnx 指令实现 在开发过程中高并发问题是很棘手的一个问题(对于博主这样的小菜鸡来说),当我们学习redis之前,知道redis是单线程运行的所以任务不会出现线程不安全问题.当我们在linux中使用ab来模拟高并发秒杀时可能会遇到两种问题,“超时和超卖”. 超时 1.redis连接超时原因 (1)虚拟机中

  • springboot +rabbitmq+redis实现秒杀示例

    目录 实现说明 1.工具准备 2.数据表 3.pom 4.代码结构 5.配置config 6.订单业务层 7.redis实现层 8.mq实现层 9.redis模拟初始化库存量 10.controller控制层 11.测试 12.测试结果 实现说明 这里的核心在于如何在大并发的情况下保证数据库能扛得住压力,因为大并发的瓶颈在于数据库.如果用户的请求直接从前端传到数据库,显然,数据库是无法承受几十万上百万甚至上千万的并发量的.因此,我们能做的只能是减少对数据库的访问.例如,前端发出了100万个请求,

  • Redis实现优惠券限一单限制详解

    需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单 我们只需要在增加订单之前,拿用户id和优惠券id判断订单是否已经存在,如果存在,说明用户已经购买. 代码实现: package com.hmdp.service.impl; import com.hmdp.dto.Result; import com.hmdp.entity.SeckillVoucher; import com.hmdp.entity.VoucherOrder; import com.hmdp.mapper.Voucher

  • 微服务Spring Boot 整合Redis 阻塞队列实现异步秒杀下单思路详解

    目录 引言 一.秒杀优化 - 异步秒杀思路 二.秒杀优化 - 基于Redis完成秒杀资格判断 三.基于阻塞队列完成异步秒杀下单 四.测试程序 五.源码地址 引言 本章节,介绍使用阻塞队列实现秒杀的优化,采用异步秒杀完成下单的优化! 一.秒杀优化 - 异步秒杀思路 当用户发起请求,此时会请求nginx,nginx会访问到tomcat,而tomcat中的程序,会进行串行操作,分成如下几个步骤 查询优惠卷 判断秒杀库存是否足够 查询订单 校验是否是一人一单 扣减库存 创建订单,完成 在以上6个步骤中,

  • Redis瞬时高并发秒杀方案总结

    1.Redis 丰富的数据结构(Data Structures) 字符串(String) Redis字符串能包含任意类型的数据;: 一个字符串类型的值最多能存储512M字节的内容: 利用INCR命令簇(INCR, DECR, INCRBY)来把字符串当作原子计数器使用: 使用APPEND命令在字符串后添加内容. 列表(List) Redis列表是简单的字符串列表,按照插入顺序排序: 你可以添加一个元素到列表的头部(左边:LPUSH)或者尾部(右边:RPUSH): 一个列表最多可以包含232-1个

随机推荐