建设工程评标专家在哪个网站登录,北京住建网站,个人做网站有什么好处,签订网站建设协议应注意事项redis优惠券秒杀
为什么订单表订单ID不采用自增长#xff1f;
id规律性太明显#xff0c;容易被用户猜测到#xff08;比如第一天下订单id10#xff0c;第二天下订单id100#xff0c;在昨天的1天内只卖出90商品#xff09;受单表数据量限制#xff08;订单数据量大
id规律性太明显容易被用户猜测到比如第一天下订单id10第二天下订单id100在昨天的1天内只卖出90商品受单表数据量限制订单数据量大日积月累单张表就存不下去这么多数据记录如果单张表存不了我们使用多张表的话表独立的自增长的话id会出现重复到时用户买到的商品有问题售后就麻烦了
这时我们就要使用全局ID生成器是一种在分布式系统下生成全局唯一ID的工具满足以下特点唯一性、高可用、高性能、递增性、安全性
为了增加id的安全性我们可以直接不使用redis自增的数值而是拼接一些其他的信息 定义一个类RedisIdWorker类并且配置Component由spring管理的bean
package com.hmdp.utils;import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;Component
public class RedisIdWorker {Resourceprivate StringRedisTemplate stringRedisTemplate;private static final long BEGIN_TIMESTAMP1672531200;private static final int COUNT_BIT32;public long nextId(String keyPrefix){// 1.生成时间戳LocalDateTime now LocalDateTime.now();long nowSecond now.toEpochSecond(ZoneOffset.UTC);long timestampnowSecond-BEGIN_TIMESTAMP;//当前时间戳// 2.生成序列号// 获取当前日期String date now.format(DateTimeFormatter.ofPattern(yyyy:MM:dd));// 自增长// 需要加上日期 因为自增上限是2^64,日积月累的销量越来越多出32位容易未来就存不下去不能使用同一个keyLong count stringRedisTemplate.opsForValue().increment(icr: keyPrefix : date);// 3.拼接并返回// 我们以为时间戳在最低位 要让他处于高位必须移位 移多少位得看给序列化号足够的位置这里是32位return timestampCOUNT_BIT|count;}/*public static void main(String[] args) {LocalDateTime time LocalDateTime.of(2023, 1, 1, 0, 0, 0);long second time.toEpochSecond(ZoneOffset.UTC);System.out.println(second);//1672531200}*/
}需要加上日期不然整个业务订单一直采用同一个key自增长时隔几年订单越来越多redis单个key的值自增上限是2的64次方好处就是每天业务订单量都是不同的key一天的订单量不会超过2^32
package com.hmdp;import com.hmdp.entity.Shop;
import com.hmdp.service.impl.ShopServiceImpl;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisIdWorker;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import javax.annotation.Resource;import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;SpringBootTest
class HmDianPingApplicationTests {Resourceprivate CacheClient cacheClient;Resourceprivate ShopServiceImpl shopService;Resourceprivate RedisIdWorker redisIdWorker;private ExecutorService es Executors.newFixedThreadPool(500);Testpublic void testIdWorker() throws InterruptedException {CountDownLatch latch new CountDownLatch(300);Runnable task () - {for (int i 0; i 100; i) {long id redisIdWorker.nextId(order);System.out.println(id id);}latch.countDown();};long begin System.currentTimeMillis();for (int i 0; i 300; i) {es.submit(task);//线程池异步所以使用CountDownLatch}latch.await();//等待所有countDown结束long end System.currentTimeMillis();System.out.println(time (end - begin));}Testvoid testSaveShop() throws InterruptedException {//shopService.saveShop2Redis(1L,10L);Shop shop shopService.getById(1L);cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY 1L, shop, 10L, TimeUnit.SECONDS);}
}添加秒杀劵这里我们没有编写后台管理系统为了方便起见使用postman测试
{shopId: 1,title: 100元代金券,subTitle: 周一到周五均可使用,rules: 全场通用\\n无需预约\\n可无限叠加\\不兑现、不找零\\n仅限堂食,payValue: 8000,actualValue: 10000,type: 1,stock: 100,beginTime:2023-02-23T19:00:00,endTime:2023-02-23T20:00:00
}注意我们点击发送请求前需要添加请求头信息 控制台打印其值即token否则会报401错 添加成功 优惠券秒杀下单功能 package com.hmdp.service.impl;import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** p* 服务实现类* /p** author 虎哥* since 2021-12-22*/
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Resourceprivate ISeckillVoucherService seckillVoucherService;Resourceprivate RedisIdWorker redisIdWorker;OverrideTransactionalpublic 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.扣减库存// eq代表where条件boolean successseckillVoucherService.update().setSql(stockstock-1).eq(voucher_id,voucherId).update();if (!success){// 扣减失败return Result.fail(库存不足);}// 6.创建订单VoucherOrder voucherOrder new VoucherOrder();// 6.1. 订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);// 6.2. 用户idLong userId UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3. 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7.返回订单Idreturn Result.ok(orderId);}
}超卖 版本号法CAS法(compare and set // 5.扣减库存// eq代表where条件boolean successseckillVoucherService.update().setSql(stockstock-1)// set stockstock-1.eq(voucher_id,voucherId).eq(stock,voucher.getStock())// where id? and stock?.update();
乐观锁成功率太低因为只要库存不一致就失败优化就是库存大于0即可
乐观锁的方案是在更新数据时候去使用的
// 5.扣减库存// eq代表where条件boolean successseckillVoucherService.update().setSql(stockstock-1)// set stockstock-1.eq(voucher_id,voucherId).gt(stock,0)// where id? and stock0.update(); 一人一单知识点动态代理、事务、aop、悲观锁、锁的范围
这里是新增数据无法判断是否有无修改过因为它不存在判断是否存在只能用悲观锁方案解决从查询订单到判断订单到新增订单这一段逻辑加上悲观锁 虽然userId值一样但是每次调用toString方法都是新的对象
dependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactId/dependency
暴露代理对象
package com.hmdp;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;EnableAspectJAutoProxy(exposeProxy true)
MapperScan(com.hmdp.mapper)
SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}}之前在方法内部添加锁可能会发生情况开启事务执行获取锁查询完减库存提交订单释放锁才会提交事务函数执行完之后由spring去提交事务锁释放意味着其他线程也可以进来了而此时事务尚未提交其他线程趁此时进来去查询订单的话刚刚新增的订单很有可能还未写进数据库因为还未提交查询依然不存在产生并发问题 可知我们刚才锁定的范围缩小了解决方案把整个函数锁起来先事务提交再释放锁那我们就要把锁放在函数外面这时就出现事务问题消失 因为此时调用方法的直接是当前目标对象IVoucherOrderService对象没有事务功能事务要想生效其实是我们spring对当前这个类进行动态代理 拿到当前类的代理对象用它来作事务处理
package com.hmdp.service.impl;import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;
import java.time.LocalDateTime;/*** p* 服务实现类* /p** author 虎哥* since 2021-12-22*/
Service
public class VoucherOrderServiceImpl extends ServiceImplVoucherOrderMapper, VoucherOrder implements IVoucherOrderService {Resourceprivate ISeckillVoucherService seckillVoucherService;Resourceprivate RedisIdWorker redisIdWorker;Overridepublic 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.一人一单Long userId UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 获取代理对象事务IVoucherOrderService proxy (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}}// 方法加上同步锁肯定是线程安全的 this指向IVoucherOrderService// 不建议加上方法上 因为代表任何用户来了都要加上锁 串行执行 性能低// 我们只需要同一用户进来才需要加锁Transactionalpublic Result createVoucherOrder(Long voucherId){// 5.一人一单Long userId UserHolder.getUser().getId();// 当用户id值一样时锁就一样// synchronized (userId.toString().intern()) {// 5.1.查询订单int count query().eq(user_id, userId).eq(voucher_id, voucherId).count();// 5.2.判断是否存在if (count 0) {//用户已经购买过return Result.fail(用户已经购买过一次);}// 6.扣减库存// eq代表where条件boolean success seckillVoucherService.update().setSql(stockstock-1)// set stockstock-1.eq(voucher_id, voucherId).gt(stock, 0)// where id? and stock0.update();if (!success) {// 扣减失败return Result.fail(库存不足);}// 7.创建订单VoucherOrder voucherOrder new VoucherOrder();// 7.1. 订单idlong orderId redisIdWorker.nextId(order);voucherOrder.setId(orderId);// 7.2. 用户idvoucherOrder.setUserId(userId);// 7.3. 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 8.返回订单Idreturn Result.ok(orderId);//}}
}上述处理方案虽然解决了一人一单的并发安全问题通过加锁可以解决单机情况下的问题但是在集群模式下就不行了 通过加锁可以解决在单机情况下的一人一单安全问题但是在集群模式下就不行了每个jvm都有自己的锁监视器集群模式下各个服务器的锁不共享。 因此我们的解决方案就是实现一个共享的锁监视器即分布式锁满足分布式系统或集群模式下多进程可见并且互斥的锁。