大技术
使用Redisson
使用Redisson在秒杀服务中有两个作用,一个是作为分布式锁来确保多个秒杀服务同时在线时同时上架秒杀商品,只允许有一个秒杀服务成功上架秒杀商品,其他的上架失败。第二个作用是作为分布式信号量,每个秒杀商品在存到Redis中时都设置一个分布式信号量,把每个秒杀商品的数量作为信号量的值,这是为了防止秒杀的时候出现穿库的情况(就是只设置了3个秒杀数量,结果秒杀结束后秒杀数量是5个,这就亏本了)
1、导入依赖
<!-- 以后使用redisson作为分布式锁,分布式对象等功能框架 --> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.16.8</version> </dependency>
2、设置Redission配置类
package com.saodai.saodaimall.saodaimall.seckill.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.io.IOException; /** * redission分布式锁配置类 */ @Configuration public class MyRedissonConfig { /** * 所有对Redisson的使用都是通过RedissonClient * @return * @throws IOException */ @Bean(destroyMethod="shutdown") public RedissonClient redisson() throws IOException { //1、创建配置 Config config = new Config(); //配置虚拟机的地址 config.useSingleServer().setAddress("redis://192.168.241.128:6379"); //2、根据Config创建出RedissonClient实例(单个实例) //Redis url should start with redis:// or rediss:// RedissonClient redissonClient = Redisson.create(config); return redissonClient; } }
3、Redission作为分布式锁
package com.saodai.saodaimall.saodaimall.seckill.scheduled; import com.saodai.saodaimall.saodaimall.seckill.service.SeckillService; import lombok.extern.slf4j.Slf4j; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; /** * 秒杀商品定时上架 * 每天晚上3点,上架最近三天需要三天秒杀的商品 * 当天00:00:00 - 23:59:59 * 明天00:00:00 - 23:59:59 * 后天00:00:00 - 23:59:59 */ @Slf4j @Service public class SeckillScheduled @Autowired private SeckillService seckillService; @Autowired private RedissonClient redissonClient; //秒杀商品上架功能的锁 private final String upload_lock = "seckill:upload:lock"; /**保证幂等性问题**/ // @Scheduled(cron = "*/5 * * * * ? ") //秒 分 时 日 月 周 @Scheduled(cron = "0 0 1/1 * * ? ") public void uploadSeckillSkuLatest3Days() { //1、重复上架无需处理 log.info("上架秒杀的商品..."); //分布式锁 RLock lock = redissonClient.getLock(upload_lock); try { //加锁(指定锁定时间为10s) lock.lock(10, TimeUnit.SECONDS); seckillService.uploadSeckillSkuLatest3Days(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
使用Redisson作为分布式锁是为了确保多个秒杀服务同时在线时同时上架秒杀商品,只允许有一个秒杀服务成功上架秒杀商品,其他的上架失败
4、Redission实现分布式信号量
作为分布式信号量,每个秒杀商品在存到Redis中时都设置一个分布式信号量,把每个秒杀商品的数量作为信号量的值,这是为了防止秒杀的时候出现穿库的情况(就是只设置了3个秒杀数量,结果秒杀结束后秒杀数量是5个,这就亏本了)
/** * 封装秒杀活动的关联商品信息到缓存里 * @param sessions 秒杀活动信息 */ private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) { if (sessions!=null){ sessions.stream().forEach(session -> { //准备hash操作,绑定hash值seckill:skus BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); //遍历秒杀活动中的商品项(seckillSkuVo表示的就是每个遍历的商品项) session.getRelationSkus().stream().forEach(seckillSkuVo -> { //生成随机码 String token = UUID.randomUUID().toString().replace("-", ""); //查看redis中有没有这个key (秒杀场次id_秒杀商品id) String redisKey = seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(); if (!operations.hasKey(redisKey)) { //缓存我们商品信息(SeckillSkuRedisTo是存入缓存中的对象) SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); Long skuId = seckillSkuVo.getSkuId(); //1、先查询sku的基本信息,调用远程服务 R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0) { SkuInfoVo skuInfo = info.getData( "skuInfo",new TypeReference<SkuInfoVo>(){}); redisTo.setSkuInfo(skuInfo); } //2、sku的秒杀信息 BeanUtils.copyProperties(seckillSkuVo,redisTo); //3、设置当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); //4、设置商品的随机码(防止恶意攻击) redisTo.setRandomCode(token); //序列化json格式存入Redis中 String seckillValue = JSON.toJSONString(redisTo); //秒杀活动的商品项的详细信息存入redis /**格式是key:4_47 value:SeckillSkuRedisTo对象的String类型**/ operations.put(seckillSkuVo.getPromotionSessionId().toString() + "-" + seckillSkuVo.getSkuId().toString(),seckillValue); //如果当前这个场次的商品库存信息已经上架就不需要上架 /**5、使用库存作为分布式Redisson信号量(限流)**/ //把每个秒杀商品的总数量作为信号量存入redis缓存,信号量标识seckill:stock:+随机(相当于key) RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); /**库存的格式key:seckill:stock:5d1df46618d34f9f9808f25cda60ba01 value:秒杀商品的总数量 其中5d1df46618d34f9f9808f25cda60ba01是随机码**/ semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); }else { log.error("没有秒杀活动"); } }
@Autowired private RedissonClient redissonClient; /**5、使用库存作为分布式Redisson信号量(限流)**/ //获取分布式信号量,信号量名称为seckill:stock:+随机码 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); // 秒杀商品的库存数量作为信号量的值(允许同时seckillSkuVo.getSeckillCount()个用户获取到信号量) semaphore.trySetPermits(seckillSkuVo.getSeckillCount());
实现uploadSeckillSkuLatest3Days方法中saveSessionSkuInfo(封装秒杀活动的关联商品信息到缓存里,每一个Redis缓存中具体sku信息用的是HashMap结构),通过HashMap结构把每个秒杀商品的详细信息以下面的格式存到Redis中,然后通过Redisson实现分布式信号量来把秒杀商品的库存总数量作为信号量存入redis缓存,每一个Redis缓存中具体sku信息的格式如下:
hash值是seckill:skus key: 4_47 value: SeckillSkuRedisTo对象
其中key的4表示秒杀的场次id,47表示秒杀商品的skuId,由于用的是hashMap结构,其中hash值是seckill:skus
Redission实现分布式信号量设置时就会把信号量以key-value的格式存到reids缓存中,Redis缓存中信号量信息的格式如下:
key: seckill:stock:随机码 value:每个秒杀商品的总数量
其中key的seckill:stock是固定前缀,随机码就是随机成功的uuid值,把每个秒杀商品的总数量作为信号量的值
准备hash操作,绑定seckill:skus关键字的hash
遍历封装存入redis的秒杀活动的秒杀商品项
生成随机码
封装SeckillSkuRedisTo对象并序列化后存入redis缓存
远程调用product商品服务
使用Redission实现分布式信号量来把秒杀商品的库存总数量作为信号量存入redis缓存(限流)
秒杀活动的商品项的详细信息存入redis缓存
设置商品的随机码(防止恶意攻击)
设置当前商品的秒杀时间信息
封装秒杀活动中秒杀商品项信息
4、秒杀时具体实现
@Autowired private RedissonClient redissonClient; //分布式锁 RSemaphore semaphore = redissonClient.getSemaphore(key); //尝试快速拿到信号量,100毫秒没有用拿到就返回false //在指定的时间内尝试地获取1个许可,如果获取不到就返回false boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);