基本流程
提供秒杀接口,利用令牌桶方式的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());
}
}
}
}