【Redis+lua】实现秒杀系统

基本流程

提供秒杀接口,利用令牌桶方式的lua脚本实现限流的功能,数据进来后,用分布式锁锁住,再对数据库进行操作。写库采用异步的方法(BlockQueue的put take),也可以直接写入mq,由另一个线程消费。

引入依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>2.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
    </dependencies>

实体类

1、商品:id 名称 总数 已卖出 乐观锁版本

@Data
public class Catalog {
    private Long id;
    private String name;
    private Long total;
    private Long sold;
    private Long version;
}

2、卖出的订单:id 商品id 商品名称 创建时间

@Data
public class SalesOrder {
    private Long id;
    private Long cid;
    private String name;
    private Date createTime;
}

Mapper

1、商品

@Mapper
public interface CatalogMapper {

    @Insert("insert into catalog (name, total, sold) values (#{name}, #{total}, #{sold}) ")
    @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
    Long insertCatalog(Catalog catalog);

    @Update("update catalog set name=#{name}, total=#{total}, sold=#{sold} where id=#{id}")
    Long updateCatalog(Catalog catalog);

    @Select("select * from catalog where id=#{id}")
    Catalog selectCatalog(@Param("id") Long id);

}

2、卖出的订单

@Mapper
public interface SalesOrderMapper {

    @Insert("insert sales_order (cid, name) values (#{cid}, #{name})")
    @Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
    void insertSalesOrder(SalesOrder salesOrder);
}

Service层

1、订单service

(1)@Transational 使用在:接口实现类 或 接口实现方法;只能应用到public方法上。

因为unchecked异常(非运行时异常)才会被spring回滚(比如runtimeException的子类,errors),但是checked异常(运行时异常)不会被spring回滚(比如IO SQL),只能用@Transactional rollbackFor进行。

一个使用了@Transational 的方法,如果方法中包含多线程使用,方法内部出现异常,也不会回滚调用方法的事务。

(2)BlockingQueue

阻塞队列,基于ReentrantLock

offer(e) put(e) offer(e,timeout,unit)
入队

队列没满,返回true

满了返回false。

不阻塞

满了一直阻塞 满了进行阻塞等待,直到超时/被唤醒/中断
poll() take() poll(e,timeout,unit)
出队

没有元素返回Null

有元素出队

队列空,一直阻塞 队列空,且超时,返回Null,未超时,等待

 项目中用到的是put和take

(3)BlockQueue的父类:ArrayBlockingQueue

多线程操作种,常用BlockQueue

抛出异常 返回特殊值(null或false) 阻塞当前线程 超时(在最大时间限制内阻塞)
插入 add() offer() put() offer
移除 remove poll take poll
检查 element peek

在秒杀系统中,多用到take移除 用put添加

@Transactional(rollbackFor = Exception.class)
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private CatalogMapper catalogMapper;
    @Autowired
    private SalesOrderMapper salesOrderMapper;
    @Autowired
    RedisTemplate<String, String> redisTemplate;

    private static final String CATALOG_TOTAL = "CATALOG_TOTAL";
    private static final String CATALOG_SOLD = "CATALOG_SOLD";
    //阻塞队列
    private BlockingQueue<Long> catalogs = new ArrayBlockingQueue<>(1000);

    /**
     * 初始化商品类别,插入到数据库
     */
    @Override
    public void initCatalog() {
        Catalog catalog = new Catalog();
        catalog.setName("mac");
        catalog.setTotal(1000L);
        catalog.setSold(0L);
        catalogMapper.insertCatalog(catalog);
        log.info("catalog:{}", catalog);
        // redis添加:total1:商品1的总数
        redisTemplate.opsForValue().set(CATALOG_TOTAL + catalog.getId(), catalog.getTotal().toString());
        //redis添加:sold1:商品1卖出的数量
        redisTemplate.opsForValue().set(CATALOG_SOLD + catalog.getId(), catalog.getSold().toString());
        log.info("redis value:{}", redisTemplate.opsForValue().get(CATALOG_TOTAL + catalog.getId()));
        //动态处理,当有订单时,将订单插入数据库中
        handleCatalog();
    }

    private void handleCatalog() {
        //动态处理
        new Thread(() -> {
            try {
                for(;;) {
                    //队列中取出商品id
                    Long catalogId = catalogs.take();
                    if(catalogId != 0) {
                        Catalog catalog = catalogMapper.selectCatalog(catalogId);
                        catalog.setSold(catalog.getSold() + 1);
                        SalesOrder salesOrder = new SalesOrder();
                        salesOrder.setCid(catalogId);
                        salesOrder.setName(catalog.getName());
                        catalogMapper.updateCatalog(catalog);
                        //订单插入数据库
                        salesOrderMapper.insertSalesOrder(salesOrder);
                        log.info("returned salesOrder.id:{}",  salesOrder.getId());
                    }
                }

            } catch (Exception e) {
                log.error("error", e);
            }
        }).start();
    }


    /**
     * 用队列处理订单
     * @param catalogId
     * @return
     */
    @Override
    public Long placeOrderWithQueue(Long catalogId) {
        String totalCache = redisTemplate.opsForValue().get(CATALOG_TOTAL + catalogId);
        String soldCache = redisTemplate.opsForValue().get(CATALOG_SOLD + catalogId);
        if(totalCache == null || soldCache == null) {
            throw new RuntimeException("Not Initialized: " + catalogId);
        }

        Integer total = Integer.parseInt(totalCache);
        Integer sold = Integer.valueOf(soldCache);

        if (total.equals(sold)){
            throw new RuntimeException("ALL SOLD OUT: " + catalogId);
        }
        try {
            //商品1放入队列
            catalogs.put(catalogId);
        } catch (Exception e) {
            log.error("error", e);
        }

        //自增
        Long soldId = redisTemplate.opsForValue().increment(CATALOG_SOLD + catalogId,1) ;
        return soldId;
    }

}

在用队列处理订单时,用到了锁:

@Slf4j
@Component
public class DistributedLock {

    //注意RedisTemplate用的String,String,后续所有用到的key和value都是String的
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    RedisScript<Boolean> lockScript;

    @Autowired
    RedisScript<Long> unlockScript;

    public Boolean distributedLock(String key, String uuid, String secondsToLock) {
        Boolean locked = false;
        try {
            String millSeconds = String.valueOf(Integer.parseInt(secondsToLock) * 1000);
            locked =redisTemplate.execute(lockScript, Collections.singletonList(key), uuid, millSeconds);
            log.info("distributedLock.key{}: - uuid:{}: - timeToLock:{} - locked:{} - millSeconds:{}",
                    key, uuid, secondsToLock, locked, millSeconds);
        } catch (Exception e) {
            log.error("error", e);
        }
        return locked;
    }

    public void distributedUnlock(String key, String uuid) {
        Long unlocked = redisTemplate.execute(unlockScript, Collections.singletonList(key),
                uuid);
        log.info("distributedUnLock.key{}: - uuid:{}: - unlocked:{}", key, uuid, unlocked);

    }
}

lua、luaConfig和限流拦截器

  • lua文件

(1)上锁

local expire = tonumber(ARGV[2])
local ret = redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', expire)
local strret = tostring(ret)
--用于查看结果,我本机获取锁成功后程序返回随机结果"table: 0x7fb4b3700fe0",否则返回"false"
redis.call('set', 'result', strret)
if strret == 'false' then
    return false
else
    return true
end
redisTemplate.execute(lockScript, Collections.singletonList(key), uuid, millSeconds);

key[1]指lockScript,argv[1]指uuid,argv[2]指millSeconds

(2)解锁

redis.call('del', 'result')
if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end
redisTemplate.execute(unlockScript, Collections.singletonList(key),uuid);

key[1]指unlockScript,argv[1]指uuid

(3)限流

--lua 下标从 1 开始
-- 限流 key
local key = KEYS[1]
-- 限流大小
local limit = tonumber(ARGV[1])

-- 获取当前流量大小
local curentLimit = tonumber(redis.call('get', key) or "0")

if curentLimit + 1 > limit then
    -- 达到限流大小 返回
    return 0;
else
    -- 没有达到阈值 value + 1
    redis.call("INCRBY", key, 1)
    -- EXPIRE后边的单位是秒
    redis.call("EXPIRE", key, ARGV[2])
    return curentLimit + 1
end

redisTemplate.execute(rateLimitScript, Collections.singletonList(key), String.valueOf(intervalPerPermit), String.valueOf(System.currentTimeMillis()),

  • lua相关的config

(1)上锁、解锁的lua

@Slf4j
@Component
public class DistributedLock {

    //注意RedisTemplate用的String,String,后续所有用到的key和value都是String的
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    RedisScript<Boolean> lockScript;

    @Autowired
    RedisScript<Long> unlockScript;

    public Boolean distributedLock(String key, String uuid, String secondsToLock) {
        Boolean locked = false;
        try {
            String millSeconds = String.valueOf(Integer.parseInt(secondsToLock) * 1000);
            locked =redisTemplate.execute(lockScript, Collections.singletonList(key), uuid, millSeconds);
            log.info("distributedLock.key{}: - uuid:{}: - timeToLock:{} - locked:{} - millSeconds:{}",
                    key, uuid, secondsToLock, locked, millSeconds);
        } catch (Exception e) {
            log.error("error", e);
        }
        return locked;
    }

    public void distributedUnlock(String key, String uuid) {
        Long unlocked = redisTemplate.execute(unlockScript, Collections.singletonList(key),
                uuid);
        log.info("distributedUnLock.key{}: - uuid:{}: - unlocked:{}", key, uuid, unlocked);

    }
}

(2)限流的Lua 

@Slf4j
@Component
public class DistributedLimit {

    //注意RedisTemplate用的String,String,后续所有用到的key和value都是String的
    @Autowired
    RedisTemplate<String, String> redisTemplate;

    @Resource
    RedisScript<Long> rateLimitScript;

    @Resource
    RedisScript<Long> limitScript;

    public Boolean distributedLimit(String key, String limit, String seconds) {
        Long id = 0L;

        try {
            id = redisTemplate.execute(limitScript, Collections.singletonList(key),
                    limit, seconds);
//            log.info("id:{}", id);
        } catch (Exception e) {
            log.error("error", e);
        }

        if(id == 0L) {
            return false;
        } else {
            return true;
        }
    }

    public Boolean distributedRateLimit(String key, String limit, String seconds) {
        Long id = 0L;
        long intervalInMills = Long.valueOf(seconds) * 1000;
        long limitInLong = Long.valueOf(limit);
        long intervalPerPermit = intervalInMills / limitInLong;
//        Long refillTime = System.currentTimeMillis();
//        log.info("调用redis执行lua脚本, {} {} {} {} {}", "ratelimit", intervalPerPermit, refillTime,
//                limit, intervalInMills);
        try {
             id = redisTemplate.execute(rateLimitScript, Collections.singletonList(key),
                    String.valueOf(intervalPerPermit), String.valueOf(System.currentTimeMillis()),
                    String.valueOf(limitInLong), String.valueOf(intervalInMills));
        } catch (Exception e) {
            log.error("error", e);
        }

        if(id == 0L) {
            return false;
        } else {
            return true;
        }
    }

}
  • 限流拦截器

(1)限流limit注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistriLimitAnno {
     String limitKey() default "limit";
     int limit() default 1;
     String seconds() default "1";
}

(2) 限流拦截器

@Pointcut("@annotation(xxx)")指捕获有xxx注解(上面limit注解)的地方

@Slf4j
@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class LimitAspect {

    @Autowired
    DistributedLimit distributedLimit;

    @Pointcut("@annotation(com.hqs.flashsales.annotation.DistriLimitAnno)")
    public void limit() {};

    @Before("limit()")
    public void beforeLimit(JoinPoint joinPoint) throws Exception {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        DistriLimitAnno distriLimitAnno = method.getAnnotation(DistriLimitAnno.class);
        String key = distriLimitAnno.limitKey();
        int limit = distriLimitAnno.limit();
        String seconds = distriLimitAnno.seconds();
        Boolean exceededLimit = distributedLimit.distributedRateLimit(key, String.valueOf(limit), seconds);
        if(!exceededLimit) {
            throw new RuntimeException("exceeded limit");
        }
    }

}

Controller

注意:要先执行/initCatalog接口,再执行/placeOrder

@Slf4j
@Controller
public class FlashSaleController {

    @Autowired
    OrderService orderService;
    @Autowired
    DistributedLock distributedLock;
    @Autowired
    LimitAspect limitAspect;
    //注意RedisTemplate用的String,String,后续所有用到的key和value都是String的
    @Autowired
    RedisTemplate<String, String> redisTemplate;

    private static final String LOCK_PRE = "LOCK_ORDER";

    @PostMapping("/initCatalog")
    @ResponseBody
    public String initCatalog()  {
        try {
            orderService.initCatalog();
        } catch (Exception e) {
            log.error("error", e);
        }

        return "init is ok";
    }

    @PostMapping("/placeOrder")
    @ResponseBody
    @DistriLimitAnno(limitKey = "limit", limit = 100, seconds = "1")
    public Long placeOrder(Long orderId) {
        Long saleOrderId = 0L;
        boolean locked = false;
        String key = LOCK_PRE + orderId;
        String uuid = String.valueOf(orderId);
        try {
            locked = distributedLock.distributedLock(key, uuid,
                    "10" );
            if(locked) {
                //直接操作数据库
//                saleOrderId = orderService.placeOrder(orderId);
                //操作缓存 异步操作数据库
                saleOrderId = orderService.placeOrderWithQueue(orderId);
            }
            log.info("saleOrderId:{}", saleOrderId);
        } catch (Exception e) {
            log.error(e.getMessage());
        } finally {
            if(locked) {
                distributedLock.distributedUnlock(key, uuid);
            }
        }
        return saleOrderId;
    }

}

测试

模拟发送/placeOrder请求

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FlashsalesApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class FlashSalesApplicationTests {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Test
    public void flashsaleTest() {
        String url = "http://localhost:8080/placeOrder";
        for(int i = 0; i < 3000; i++) {
            try {
                TimeUnit.MILLISECONDS.sleep(20);
                new Thread(() -> {
                    MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
                    params.add("orderId", "1");
                    //模拟发送请求
                    Long result = testRestTemplate.postForObject(url, params, Long.class);
                    if(result != 0) {
                        System.out.println("-------------" + result);
                    }
                }
                ).start();
            } catch (Exception e) {
                log.info("error:{}", e.getMessage());
            }

        }
    }

}

猜你喜欢

转载自blog.csdn.net/kanseu/article/details/124139523