spring boot redis 商品秒杀设计
商品秒杀多数发生在高并发的时候,本文利用redis单线程原子性解决并发,减库存;
思路方案:
1. 模拟100个用户下单抢10个商品;
2.使用redis加锁,来实现减去库存;
3.其他用户一直等待,直到解锁后后面用户再加锁减库存,依次操作,直到库存为0;
4.用户等待,设置一下超时时间,防止一直等待下去;
5.判断库存是否小于等于0;
其中超时代码中有不要使用System.currentTimeMillis,高并发下性能很差,解决方案写了一个定时任务,1毫米间隔定时执行,然后去获取,代码SystemTimeUtil.now;
1.自定义SystemTimeUtil类,实现获取当前时间戳
package shop.seckill.example.utils; import org.apache.commons.lang3.time.DateFormatUtils; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; /** * @author IT006448 * @date 2022/01/17 */ public class SystemTimeUtil { private final int period; private final AtomicLong now; private static class InstanceHolder { private static final SystemTimeUtil INSTANCE = new SystemTimeUtil(1); } //定时任务设置1毫秒 private SystemTimeUtil(int period) { this.period = period; this.now = new AtomicLong(System.currentTimeMillis()); scheduleClockUpdating(); } private static SystemTimeUtil instance() { return InstanceHolder.INSTANCE; } private void scheduleClockUpdating() { //周期执行线程池 ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> { Thread thread = new Thread(runnable, "System Clock"); //守护线程 thread.setDaemon(true); return thread; }); //任务,开始时间,间隔时间=周期执行,时间单位 scheduler.scheduleAtFixedRate(() -> now.set(System.currentTimeMillis()), 0, period, TimeUnit.MILLISECONDS); } private long currentTimeMillis() { return now.get(); } /** * 用来替换原来的System.currentTimeMillis() */ public static long now() { return instance().currentTimeMillis(); } public static String nowTime() { long now = now(); return DateFormatUtils.format(now, "yyyy-MM-dd hh:mm:ss"); } public static void main(String[] args) throws InterruptedException { String now = SystemTimeUtil.nowTime(); System.out.println(now); Thread.sleep(1000); now = SystemTimeUtil.nowTime(); System.out.println(now); Thread.sleep(2000); now = SystemTimeUtil.nowTime(); System.out.println(now); } }
2.秒杀核心
创建一个秒杀服务接口
public interface SecKillService { void buy(String goodsId, String userId); }
package shop.seckill.example.service.imp; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import com.google.common.collect.Maps; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import scf.frame.starter.cache.redis.JedisUtil; import shop.seckill.example.service.SecKillService; import shop.seckill.example.utils.SystemTimeUtil; import java.util.Date; import java.util.Map; /** * @author IT006448 * @date 2022/01/14 */ @Service @Slf4j public class SecKillServiceImpl implements SecKillService { @Autowired private JedisUtil jedisUtil; public static Map<String, Integer> inventoryMap = Maps.newLinkedHashMap(); { inventoryMap.put("10001", 10); } @Override public void buy(String goodsId, String userId) { String key = "SECKILL_USER_".concat(goodsId); //用户开抢时间 //long startTime = System.currentTimeMillis(); //System.currentTimeMillis并发性能太差,用定时任务去获取,性能更好 long startTime = SystemTimeUtil.now(); int timeout = 10000; //未抢到的情况下,10秒内继续获取锁 while ((startTime + timeout) >= SystemTimeUtil.now()) { String strInventory = jedisUtil.getStr(goodsId + "_INVENTORY_NUM"); //商品是否剩余 System.out.println(getDate() + " 库存数量:" + strInventory); if (strInventory != null && Integer.parseInt(strInventory) <= 0) { System.out.println("商品没有剩余了,跳出"); break; } //获取锁 boolean lock = jedisUtil.setIfAbsent(key, userId); if (lock) { //加锁成功 try { jedisUtil.expire(key, 60); parallelBuy(goodsId, userId); return; } finally { jedisUtil.del(key); } } else { System.out.println(String.format("用户:%s锁了,等待买", userId)); } } } public void parallelBuy(String goodsId, String userId) { Integer inventory = inventoryMap.get(goodsId); try { //模拟减去库存等操作耗时 Thread.sleep(1000); } catch (Exception e) { } if (inventory <= 0) { System.out.println("商品已经卖完"); return; //throw new RuntimeException("商品:".concat(goodsId).concat("已经卖完")); } --inventory; jedisUtil.set(goodsId + "_INVENTORY_NUM", inventory.toString(), 60); System.out.println(getDate() + " 还剩下:".concat(inventory.toString())); System.out.println(String.format(getDate() + " 用户:%s买了一件", userId)); inventoryMap.put(goodsId, inventory); } public String getDate() { return JSON.toJSONString(new Date(), SerializerFeature.UseISO8601DateFormat); } }
3.模拟创建100个随机用户,并并发执行
package shop.seckill.example.service.imp; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; import shop.seckill.example.service.SecKillService; import java.util.LinkedList; import java.util.List; import java.util.stream.IntStream; /** * @author IT006448 * @date 2022/01/14 */ @Service @Slf4j public class ShopServiceImpl implements ShopService { @Autowired private SecKillService secKillService; @Override public void buy() { List<String> user = createUser(); StopWatch stopWatch = new StopWatch(); stopWatch.start(); log.info("用户:{}", JSON.toJSONString(user)); user.parallelStream().forEach(x -> { secKillService.buy("10001", x); }); stopWatch.stop(); double totalTimeSeconds = stopWatch.getTotalTimeSeconds(); System.out.println("运行总时间:" + totalTimeSeconds); System.out.println("库存数量:" + SecKillServiceImpl.inventoryMap.get("10001")); } public List<String> createUser() { List<String> user = new LinkedList<>(); IntStream.range(0, 100).parallel().forEach(x -> { user.add("用户" + x); }); return user; } }