一、技术栈
springboot+mybatis+redis
二、实现原理
以商品库存为例:
更新商品库存的时候,根据数据(如商品ID )的唯一标识,将操作路由之后,发送到一个jvm内部的队列中
读取数据(库存)的时候,如果发现数据不在缓存(redis)中,那么将执行重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,一条一条的执行
一个数据变更(更改库存)的操作,先执行删除缓存,然后再去更新数据库,但是还没完成更新,此时如果一个读请求(读取库存)过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成
优化点: 一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可,待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中,如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
三、实现步骤
1:线程池+内存队列初始化
2:两种请求(读写)对象封装
3:请求异步执行Service封装
4:请求处理的工作线程封装
5:两种请求Controller接口封装
6:读请求去重优化
四、代码实现:
线程池+内存队列初始化:
在springboot启动类中注册监听器,启动时初始化线程池与内存队列
@Bean
public ServletListenerRegistrationBean servletListenerRegistrationBean(){
ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean();
servletListenerRegistrationBean.setListener(new InitListener());
return servletListenerRegistrationBean;
}
public class InitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
//初始化方法
RequestProcessorThreadPool.init();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
初始化线程池,采用内部类的方式单例初始化,初始化内存队列,并将内存队列与线程绑定
/**
* 请求处理线程池
*/
public class RequestProcessorThreadPool {
/**
* 线程池
*/
private ExecutorService threadPool = Executors.newFixedThreadPool(10);
/**
* 构造方法绑定线程与队列
*/
public RequestProcessorThreadPool(){
for(int i = 0; i < 10; i++){
ArrayBlockingQueue<Request> queue = new ArrayBlockingQueue<Request>(100);
RequestQueue requestQueue = RequestQueue.getInstance();
requestQueue.addQueues(queue);
threadPool.submit(new RequestProcessorThread(queue));
}
}
private static class Singleton{
private static RequestProcessorThreadPool instance;
static {
instance = new RequestProcessorThreadPool();
}
public static RequestProcessorThreadPool getInstance(){
return instance;
}
}
public static RequestProcessorThreadPool getInstance(){
return Singleton.getInstance();
}
public static RequestProcessorThreadPool init(){
return getInstance();
}
}
/**
* 请求内存队列
*/
public class RequestQueue {
private List<ArrayBlockingQueue<Request>> queues = new ArrayList<>();
//维护标识过滤请求优化
private Map<Integer,Boolean> flagMap = new ConcurrentHashMap<>();
private static class Singleton{
private static RequestQueue queues;
static {
queues = new RequestQueue();
}
public static RequestQueue getInstance(){
return queues;
}
}
/**
* 初始化
* @return
*/
public static RequestQueue getInstance(){
return Singleton.getInstance();
}
/**
* 添加队列
* @param queue
*/
public void addQueues(ArrayBlockingQueue<Request> queue){
this.queues.add(queue);
}
/**
* 获取内存队列大小
* @return
*/
public int queuesSize(){
return this.queues.size();
}
/**
* 获取内存队列
* @param index
* @return
*/
public ArrayBlockingQueue<Request> getQueue(int index){
return this.queues.get(index);
}
/**
* 获取标识
* @return
*/
public Map<Integer, Boolean> getFlagMap() {
return flagMap;
}
}
两种请求(读写)对象封装
接口类
/**
* 请求对象
*/
public interface Request {
void process();
Integer getProductId();
Boolean getFlag();
}
读请求
public class ProductStockCacheRefreshRequest implements Request {
//库存操作service
private ProductStockService productStockService;
//商品id
private Integer productId;
//强制刷新缓存标识位
private Boolean flag;
public ProductStockCacheRefreshRequest(ProductStockService productStockService,Integer productId,Boolean flag){
this.productId = productId;
this.productStockService = productStockService;
this.flag = flag;
}
@Override
public void process() {
//获取DB库存
ProductStock productStock = productStockService.findProductStock(productId);
//将DB库存刷新至缓存
productStockService.setProductStockCache(productStock);
}
@Override
public Integer getProductId() {
return this.productId;
}
@Override
public Boolean getFlag() {
return this.flag;
}
}
写请求
public class ProductStockDBUpdateRequest implements Request {
//库存操作service
private ProductStockService productStockService;
//库存信息
private ProductStock productStock;
public ProductStockDBUpdateRequest(ProductStockService productStockService,ProductStock productStock){
this.productStock = productStock;
this.productStockService = productStockService;
}
@Override
public void process() {
//删除缓存
productStockService.removeProductStockCache(productStock);
//更新数据库库存
productStockService.updateProductStock(productStock);
}
@Override
public Integer getProductId() {
return this.productStock.getProductId();
}
@Override
public Boolean getFlag() {
return false;
}
}
请求异步执行Service封装
public interface RequestAsyncProcessService {
void process(Request request);
}
@Service
public class RequestAsyncProcessServiceImpl implements RequestAsyncProcessService {
@Override
public void process(Request request) {
try {
//获取到商品指定的路由队列
ArrayBlockingQueue<Request> queue = getRoutingQueue(request.getProductId());
//将商品添加到该队列等待处理
queue.put(request);
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 获取路由到的内存队列
* @param productId 商品id
* @return 内存队列
*/
private ArrayBlockingQueue<Request> getRoutingQueue(Integer productId) {
RequestQueue requestQueue = RequestQueue.getInstance();
// 先获取productId的hash值
String key = String.valueOf(productId);
int h;
int hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// 对hash值取模,将hash值路由到指定的内存队列中,比如内存队列大小8
// 用内存队列的数量对hash值取模之后,结果一定是在0~7之间
// 所以任何一个商品id都会被固定路由到同样的一个内存队列中去的
int index = (requestQueue.queuesSize() - 1) & hash;
//log.info("路由内存队列,商品id:{},索引:{}",productId,index);
return requestQueue.getQueue(index);
}
}
请求处理的工作线程封装(包含读请求去重优化)
public class RequestProcessorThread implements Callable<Boolean> {
private ArrayBlockingQueue<Request> queue;
public RequestProcessorThread(ArrayBlockingQueue<Request> queue){
this.queue = queue;
}
@Override
public Boolean call(){
try {
while (true){
Request request = queue.take();
Map<Integer,Boolean> flagMap = RequestQueue.getInstance().getFlagMap();
if(request instanceof ProductStockDBUpdateRequest){
//写请求
flagMap.put(request.getProductId(),true);
request.process();
}
else{
//读请求过滤
if(!request.getFlag()){ //不需强制刷新缓存的做读请求过滤
Boolean flag = flagMap.get(request.getProductId());
//读请求
if(null == flag){
//空请求算作读缓存
flagMap.put(request.getProductId(),false);
request.process();
}
if(null != flag && flag){
flagMap.put(request.getProductId(),false);
request.process();
}
System.out.println("当前读请求已过滤...");
//if(null != flag && !flag){//过滤不做处理}
}else{//强制刷新缓存
flagMap.put(request.getProductId(),false);
request.process();
}
}
}
}catch (Exception e){
e.printStackTrace();
}
return true;
}
}
两种请求Controller接口封装
@RestController
public class ProductStockController {
@Autowired
private ProductStockService productStockService;
@Autowired
private RequestAsyncProcessService requestAsyncProcessService;
/**
* 更新商品库存
*/
@PutMapping("/product/stock")
public Response updateProductInventory(ProductStock productStock) {
Response response = null;
try {
Request request = new ProductStockDBUpdateRequest(productStockService,productStock);
requestAsyncProcessService.process(request);
response = new Response(Response.SUCCESS);
} catch (Exception e) {
e.printStackTrace();
response = new Response(Response.FAIL);
}
return response;
}
@GetMapping("/product/{id}")
public Response getProductStock(@PathVariable("id")Integer id){
try {
Request request = new ProductStockCacheRefreshRequest(productStockService,id,false);
requestAsyncProcessService.process(request);
long startTime = System.currentTimeMillis();
long endTime = 0L;
long waitTime = 0L;
while (waitTime < 200){
//尝试从缓存获取数据
ProductStock productStock = productStockService.getProductStockCache(id);
if(null != productStock){
return new Response(Response.SUCCESS,productStock);
}
try {
Thread.sleep(20);
}catch (Exception e){
e.printStackTrace();
}
endTime = System.currentTimeMillis();
waitTime = endTime - startTime;
}
//200毫秒内未获取到数据,从DB中获取并返回
ProductStock productStock = productStockService.findProductStock(id);
if(null != productStock){
// 将缓存刷新一下,并放入队列串行处理
// 为防止redis的LRU算法清理掉数据,导致上次的读请求的标志位在false上被过滤,这里需要强制刷新缓存
request = new ProductStockCacheRefreshRequest(productStockService,id,true);
requestAsyncProcessService.process(request);
return new Response(Response.SUCCESS,productStock);
}
}catch (Exception e){
e.printStackTrace();
}
return new Response(Response.SUCCESS,-1L);
}
}
五、说明
核心代码在文中已经贴出,其余service代码等,就不在这里放上去了,完整实现demo见GitHub地址