[一个简单的秒杀架构的演变]3. 使用缓存

地址

目录

1. 思路介绍

这次我们引入缓存,这里可以先查看一篇文章: Redis与数据库一致性

一般秒杀都会提前预热缓存,我们添加一个缓存预热的方法,初始化库存后再缓存预热,这样不会出现Cache读取Miss的情况

这里我采用的是先更新数据库再更新缓存,因为这里缓存数据计算简单,只需要进行加减一即可,所以我们直接进行更新缓存

这次主要改造是检查库存和扣库存方法,检查库存直接去Redis获取,不再去查数据库,而在扣库存这里本身是使用的乐观锁操作,只有操作成功(扣库存成功)的才需要更新缓存数据

2. 代码实现

引入Redis

  • pom.xml
<jedis.version>2.9.0</jedis.version>
<!-- Redis-Jedis -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>${jedis.version}</version>
</dependency>
  • application.yml
redis:
    # Redis服务器地址
    host: 127.0.0.1
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # 连接超时时间(毫秒)
    timeout: 10000
    pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 200
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0
  • JedisConfig
package com.example.config.redis;

import ...;

/**
 * Jedis配置,项目启动注入JedisPool
 *
 * @author dolyw.com
 * @date 2018/9/5 10:35
 */
@Configuration
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "redis")
public class JedisConfig {

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(JedisConfig.class);

    private String host;

    private int port;

    private String password;

    private int timeout;

    @Value("${redis.pool.max-active}")
    private int maxActive;

    @Value("${redis.pool.max-wait}")
    private int maxWait;

    @Value("${redis.pool.max-idle}")
    private int maxIdle;

    @Value("${redis.pool.min-idle}")
    private int minIdle;

    @Bean
    public JedisPool redisPoolFactory() {
        try {
            JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
            jedisPoolConfig.setMaxIdle(maxIdle);
            jedisPoolConfig.setMaxWaitMillis(maxWait);
            jedisPoolConfig.setMaxTotal(maxActive);
            jedisPoolConfig.setMinIdle(minIdle);
            // 密码为空设置为null
            if (StringUtils.isBlank(password)) {
                password = null;
            }
            JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
            logger.info("初始化Redis连接池JedisPool成功!地址: " + host + ":" + port);
            return jedisPool;
        } catch (Exception e) {
            logger.error("初始化Redis连接池JedisPool异常:" + e.getMessage());
        }
        return null;
    }

    // GETSET...
}

先定义三个全局常量

  • Constant
package com.example.constant;

/**
 * 常量
 *
 * @author wliduo[i@dolyw.com]
 * @date 2018/9/3 16:03
 */
public interface Constant {

    // 其他常量...

    /**
     * redis-key-前缀-count-库存
     */
    String PREFIX_COUNT = "stock:count:";

    /**
     * redis-key-前缀-sale-已售
     */
    String PREFIX_SALE = "stock:sale:";

    /**
     * redis-key-前缀-version-乐观锁版本
     */
    String PREFIX_VERSION = "stock:version:";

}

SeckillEvolutionController创建缓存预热方法,以及乐观锁加缓存下单的入口方法

  • SeckillEvolutionController
/**
 * 缓存预热
 *
 * @param id 商品ID
 * @return com.example.common.ResponseBean
 * @throws
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 15:59
 */
@PutMapping("/initCache/{id}")
public ResponseBean initCache(@PathVariable("id") Integer id) {
    StockDto stockDto = stockService.selectByPrimaryKey(id);
    // 商品缓存预热
    JedisUtil.set(Constant.PREFIX_COUNT + id.toString(), stockDto.getCount().toString());
    JedisUtil.set(Constant.PREFIX_SALE + id.toString(), stockDto.getSale().toString());
    JedisUtil.set(Constant.PREFIX_VERSION + id.toString(), stockDto.getVersion().toString());
    return new ResponseBean(HttpStatus.OK.value(), "缓存预热成功", null);
}

/**
 * 使用乐观锁下订单,并且添加读缓存,性能提升
 *
 * @param id 商品ID
 * @return com.example.common.ResponseBean
 * @throws Exception
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:24
 */
@PostMapping("/createOptimisticLockOrderWithRedis/{id}")
public ResponseBean createOptimisticLockOrderWithRedis(@PathVariable("id") Integer id) throws Exception {
    // 错误的,线程不安全
    Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisWrong(id);
    // 正确的,线程安全
    // Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisSafe(id);
    return new ResponseBean(HttpStatus.OK.value(), "购买成功", null);
}

Service添加两个方法(缓存线程安全和不安全的两个方法)

  • ISeckillEvolutionService
/**
 * 使用乐观锁创建订单(解决卖超问题),加缓存读(线程不安全),提升性能,
 *
 * @param id
 * @return java.lang.Integer
 * @throws Exception
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:21
 */
Integer createOptimisticLockOrderWithRedisWrong(Integer id) throws Exception;

/**
 * 使用乐观锁创建订单(解决卖超问题),加缓存读(线程安全),提升性能,
 *
 * @param id
 * @return java.lang.Integer
 * @throws Exception
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:21
 */
Integer createOptimisticLockOrderWithRedisSafe(Integer id) throws Exception;
  • ISeckillEvolutionService
/**
 * 乐观锁加缓存方法(名称注入),线程不安全
 */
@Autowired
@Qualifier("seckillOptimisticLockRedisWrongService")
private ISeckillService SeckillOptimisticLockRedisWrongServiceImpl;

/**
 * 乐观锁加缓存方法(名称注入),线程安全
 */
@Autowired
@Qualifier("seckillOptimisticLockRedisSafeService")
private ISeckillService seckillOptimisticLockRedisSafeService;

@Override
@Transactional(rollbackFor = Exception.class)
public Integer createOptimisticLockOrderWithRedisWrong(Integer id) throws Exception {
    // 检查库存
    StockDto stockDto = SeckillOptimisticLockRedisWrongServiceImpl.checkStock(id);
    // 扣库存
    Integer saleCount = SeckillOptimisticLockRedisWrongServiceImpl.saleStock(stockDto);
    if (saleCount <= 0) {
        throw new CustomException("扣库存失败");
    }
    Thread.sleep(10);
    // 下订单
    Integer orderCount = SeckillOptimisticLockRedisWrongServiceImpl.createOrder(stockDto);
    if (saleCount <= 0) {
        throw new CustomException("下订单失败");
    }
    return orderCount;
}

@Override
@Transactional(rollbackFor = Exception.class)
public Integer createOptimisticLockOrderWithRedisSafe(Integer id) throws Exception {
    // 检查库存
    StockDto stockDto = seckillOptimisticLockRedisSafeService.checkStock(id);
    // 扣库存
    Integer saleCount = seckillOptimisticLockRedisSafeService.saleStock(stockDto);
    if (saleCount <= 0) {
        throw new CustomException("扣库存失败");
    }
    Thread.sleep(10);
    // 下订单
    Integer orderCount = seckillOptimisticLockRedisSafeService.createOrder(stockDto);
    if (saleCount <= 0) {
        throw new CustomException("下订单失败");
    }
    Thread.sleep(10);
    return orderCount;
}

3. 错误实现

然后创建一个ISeckillService的使用乐观锁方式的实现类(线程不安全)

  • SeckillOptimisticLockRedisWrongServiceImpl
package com.example.seckill.impl;

import ...;

import java.util.List;

/**
 * 使用乐观锁加缓存
 *
 * @author wliduo[i@dolyw.com]
 * @date 2019-11-20 18:03:33
 */
@Service("seckillOptimisticLockRedisWrongService")
public class SeckillOptimisticLockRedisWrongServiceImpl implements ISeckillService {

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(SeckillOptimisticLockRedisServiceImpl.class);

    @Autowired
    private StockDao stockDao;

    @Autowired
    private StockOrderDao stockOrderDao;

    @Override
    public StockDto checkStock(Integer id) {
        // 使用缓存读取库存,减轻DB压力
        Integer count = Integer.parseInt(JedisUtil.get(Constant.PREFIX_COUNT + id));
        Thread.sleep(100);
        Integer sale = Integer.parseInt(JedisUtil.get(Constant.PREFIX_SALE + id));
        Thread.sleep(100);
        // 第一个线程和第二个线程同时读取缓存count时,都读取到10,然后第二个线程暂停了,第一个线程继续执行,
        // 读取的version版本号为0,继续执行到已经秒杀完成,更新缓存(version版本号加一,变成1),
        // 现在第二个线程才恢复继续执行,结果读取缓存version版本号为1(本来应该也是0)
        Integer version = Integer.parseInt(JedisUtil.get(Constant.PREFIX_VERSION + id));
        if (count > 0) {
            // 还有库存
            StockDto stockDto = new StockDto();
            stockDto.setId(id);
            stockDto.setCount(count);
            stockDto.setSale(sale);
            stockDto.setVersion(version);
            return stockDto;
        }
        throw new CustomException("库存不足");
    }

    @Override
    public Integer saleStock(StockDto stockDto) {
        Integer saleCount = stockDao.updateByOptimisticLock(stockDto);
        // 操作数据大于0,说明扣库存成功
        if (saleCount > 0) {
            logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion());
            Thread.sleep(10);
            // 更新缓存,这里更新需要保证三个数据(库存,已售,乐观锁版本号)的一致性,使用Redis事务
            updateCache(stockDto);
        }
        return saleCount;
    }

    /**
     * 这里遵循先更新数据库,再更新缓存,详细的数据库与缓存一致性解析可以查看
     * https://note.dolyw.com/cache/00-DataBaseConsistency.html
     *
     * @param stockDto
     * @return java.lang.Integer
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/22 16:25
     */
    public void updateCache(StockDto stockDto) {
        // 获取事务
        Transaction transaction = null;
        try (Jedis jedis = JedisUtil.getJedis()) {
            // watch监视key,当事务执行之前这个key发生了改变,事务会被打断
            jedis.watch(Constant.PREFIX_COUNT + stockDto.getId(),
                    Constant.PREFIX_SALE + stockDto.getId(),
                    Constant.PREFIX_VERSION + stockDto.getId());
            // 开始事务
            transaction = jedis.multi();
            // 原子操作库存减一
            transaction.decr(Constant.PREFIX_COUNT + stockDto.getId());
            // 原子操作已售数和乐观锁版本号加一
            transaction.incr(Constant.PREFIX_SALE + stockDto.getId());
            transaction.incr(Constant.PREFIX_VERSION + stockDto.getId());
            // 执行exec后就会自动执行jedis.unwatch()操作
            List<Object> result = transaction.exec();
            if (result == null || result.isEmpty()) {
                // 事务失败了,可能是watch-key被外部修改,或者是数据操作被驳回
                logger.error("更新缓存失败: {}", stockDto.toString());
                // watch-key被外部修改时,transaction.discard()操作会被自动触发
            }
        } catch (Exception e) {
            logger.error("更新缓存失败: {},异常原因: {}", stockDto.toString(), e.getMessage());
            if (transaction != null) {
                // 关闭事务
                transaction.discard();
            }
        }
    }

    @Override
    public Integer createOrder(StockDto stockDto) {
        StockOrderDto stockOrderDto = new StockOrderDto();
        stockOrderDto.setStockId(stockDto.getId());
        return stockOrderDao.insertSelective(stockOrderDto);
    }

}

3.1. 开始测试

修改SeckillEvolutionController的入口下单方法createOptimisticLockOrderWithRedis调用createOptimisticLockOrderWithRedisWrong线程不安全方法

/**
 * 使用乐观锁下订单,并且添加读缓存,性能提升
 *
 * @param id 商品ID
 * @return com.example.common.ResponseBean
 * @throws Exception
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:24
 */
@PostMapping("/createOptimisticLockOrderWithRedis/{id}")
public ResponseBean createOptimisticLockOrderWithRedis(@PathVariable("id") Integer id) throws Exception {
    // 错误的,线程不安全
    Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisWrong(id);
    // 正确的,线程安全
    // Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisSafe(id);
    return new ResponseBean(HttpStatus.OK.value(), "购买成功", null);
}

使用JMeter测试上面的代码,JMeter的使用可以查看: JMeter的安装使用

我们调用一下商品库存初始化的方法,我使用的是PostMan,初始化库存表商品10个库存,而且清空订单表

图片

接着使用PostMan调用缓存预热方法,提前加载好缓存

图片

这时候可以看到我们的数据,库存为10,卖出为0,订单表为空

图片

缓存数据也是这样

图片

打开JMeter,添加测试计划(测试计划文件在项目的src\main\resources\jmx下),模拟500个并发线程测试秒杀10个库存的商品,填写请求地址,点击启动图标开始

图片
图片

可以看到500个并发线程执行完,最后发现库存出现负数

图片

3.2. 结果总结

// 使用缓存读取库存,减轻DB压力
Integer count = Integer.parseInt(JedisUtil.get(Constant.PREFIX_COUNT + id));
Integer sale = Integer.parseInt(JedisUtil.get(Constant.PREFIX_SALE + id));
Integer version = Integer.parseInt(JedisUtil.get(Constant.PREFIX_VERSION + id));

一开始没考虑好,结果发现这块读取缓存代码会存在线程不安全的情况,比如有个线程读取Residcount库存时,正好另一个已经执行的前面的线程更新缓存事务提交了,结果导致读取到version版本号与库存不一致,不是同一个事务的数据了

logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion());
2019-11-22 20:42:17.874  INFO --- [nio-8080-exec-2] : 版本号:10 0 0
2019-11-22 20:42:17.910  INFO --- [io-8080-exec-10] : 版本号:10 0 1
2019-11-22 20:42:17.964  INFO --- [io-8080-exec-26] : 版本号:10 0 2
2019-11-22 20:42:18.010  INFO --- [io-8080-exec-14] : 版本号:10 1 3
2019-11-22 20:42:18.033  INFO --- [io-8080-exec-15] : 版本号:10 1 4
2019-11-22 20:42:18.085  INFO --- [io-8080-exec-20] : 版本号:10 2 5
2019-11-22 20:42:18.108  INFO --- [io-8080-exec-23] : 版本号:9 3 6
2019-11-22 20:42:18.145  INFO --- [io-8080-exec-19] : 版本号:8 5 7
2019-11-22 20:42:18.169  INFO --- [io-8080-exec-17] : 版本号:8 5 8
2019-11-22 20:42:18.198  INFO --- [io-8080-exec-60] : 版本号:7 5 9
2019-11-22 20:42:18.236  INFO --- [io-8080-exec-39] : 版本号:6 7 10
2019-11-22 20:42:18.282  INFO --- [io-8080-exec-40] : 版本号:5 8 11
2019-11-22 20:42:18.311  INFO --- [io-8080-exec-48] : 版本号:4 10 12
2019-11-22 20:42:18.336  INFO --- [io-8080-exec-51] : 版本号:3 10 13
2019-11-22 20:42:18.372  INFO --- [io-8080-exec-55] : 版本号:2 11 14
2019-11-22 20:42:18.396  INFO --- [io-8080-exec-52] : 版本号:1 12 15

::: tip 分析
看日志,详细分析下错误流程,例如有两个线程启动,第一个线程和第二个线程同时读取缓存count时,都读取到10,然后第二个线程暂停了,第一个线程继续执行,读取的version版本号为0,继续执行到已经秒杀完成,更新缓存(version版本号加一,变成1),现在第二个线程才恢复继续执行,结果读取缓存version版本号为1(不是同一个事务的数据,本来应该也是0),所以变成了10 0 1的错误数据
:::

3.3. 如何解决

3.3.1. 乐观锁扣库存时加上库存大于0的判断,AND count > 0

  • StockDao
/**
 * 乐观锁更新扣减库存
 *
 * @param stockDto
 * @return int
 * @throws
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:14
 */
@Update("UPDATE t_seckill_stock SET count = count - 1, sale = sale + 1, version = version + 1 " +
        "WHERE id = #{id, jdbcType = INTEGER} AND version = #{version, jdbcType = INTEGER} " +
        "AND count > 0")
int updateByOptimisticLock(StockDto stockDto);

3.3.2. 给这块读取代码加上Redis事务(乐观锁)或者分布式锁(悲观锁)

// 使用缓存读取库存,减轻DB压力
Integer count = Integer.parseInt(JedisUtil.get(Constant.PREFIX_COUNT + id));
Integer sale = Integer.parseInt(JedisUtil.get(Constant.PREFIX_SALE + id));
Integer version = Integer.parseInt(JedisUtil.get(Constant.PREFIX_VERSION + id));

3.3.3. 使用Redis批量操作(mget和mset)

mgetmset可以批量获取和设置Redis键值,具有原子性,这种方式更简单,就先用这个了,看下面正确实现

4. 正确实现

  • SeckillOptimisticLockRedisSafeServiceImpl
package com.example.seckill.impl;


import com.example.constant.Constant;
import com.example.dao.StockDao;
import com.example.dao.StockOrderDao;
import com.example.dto.custom.StockDto;
import com.example.dto.custom.StockOrderDto;
import com.example.exception.CustomException;
import com.example.seckill.ISeckillService;
import com.example.util.JedisUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

import java.util.List;

/**
 * 使用乐观锁加缓存的方式(线程安全)(使用Redis批量操作mget和mset具有原子性)
 *
 * @author wliduo[i@dolyw.com]
 * @date 2019-11-20 18:03:33
 */
@Service("seckillOptimisticLockRedisSafeService")
public class SeckillOptimisticLockRedisSafeServiceImpl implements ISeckillService {

    /**
     * logger
     */
    private static final Logger logger = LoggerFactory.getLogger(SeckillOptimisticLockRedisSafeServiceImpl.class);

    @Autowired
    private StockDao stockDao;

    @Autowired
    private StockOrderDao stockOrderDao;

    @Override
    public StockDto checkStock(Integer id) throws Exception {
        // 使用缓存读取库存,减轻DB压力,Redis批量操作(具有原子性)解决线程安全问题
        List<String> dataList = JedisUtil.mget(Constant.PREFIX_COUNT + id,
                Constant.PREFIX_SALE + id, Constant.PREFIX_VERSION + id);
        Integer count = Integer.parseInt(dataList.get(0));
        Integer sale = Integer.parseInt(dataList.get(1));
        Integer version = Integer.parseInt(dataList.get(2));
        if (count > 0) {
            // 还有库存
            StockDto stockDto = new StockDto();
            stockDto.setId(id);
            stockDto.setCount(count);
            stockDto.setSale(sale);
            stockDto.setVersion(version);
            Thread.sleep(10);
            return stockDto;
        }
        throw new CustomException("库存不足");
    }

    @Override
    public Integer saleStock(StockDto stockDto) throws Exception {
        Integer saleCount = stockDao.updateByOptimisticLock(stockDto);
        // 操作数据大于0,说明扣库存成功
        if (saleCount > 0) {
            logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion());
            Thread.sleep(10);
            // 更新缓存,这里更新需要保证三个数据(库存,已售,乐观锁版本号)的一致性,使用mset原子操作
            updateCache(stockDto);
        }
        return saleCount;
    }

    /**
     * 这里遵循先更新数据库,再更新缓存,详细的数据库与缓存一致性解析可以查看
     * https://note.dolyw.com/cache/00-DataBaseConsistency.html
     *
     * @param stockDto
     * @return java.lang.Integer
     * @throws
     * @author wliduo[i@dolyw.com]
     * @date 2019/11/22 16:25
     */
    public void updateCache(StockDto stockDto) {
        Integer count = stockDto.getCount() - 1;
        Integer sale = stockDto.getSale() + 1;
        Integer version = stockDto.getVersion() + 1;
        JedisUtil.mset(Constant.PREFIX_COUNT + stockDto.getId(), count.toString(),
                Constant.PREFIX_SALE + stockDto.getId(), sale.toString(),
                Constant.PREFIX_VERSION + stockDto.getId(), version.toString());
    }

    @Override
    public Integer createOrder(StockDto stockDto) {
        StockOrderDto stockOrderDto = new StockOrderDto();
        stockOrderDto.setStockId(stockDto.getId());
        return stockOrderDao.insertSelective(stockOrderDto);
    }

}

4.1. 开始测试

修改SeckillEvolutionController的入口下单方法createOptimisticLockOrderWithRedis调用createOptimisticLockOrderWithRedisSafe线程安全方法

/**
 * 使用乐观锁下订单,并且添加读缓存,性能提升
 *
 * @param id 商品ID
 * @return com.example.common.ResponseBean
 * @throws Exception
 * @author wliduo[i@dolyw.com]
 * @date 2019/11/22 14:24
 */
@PostMapping("/createOptimisticLockOrderWithRedis/{id}")
public ResponseBean createOptimisticLockOrderWithRedis(@PathVariable("id") Integer id) throws Exception {
    // 错误的,线程不安全
    // Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisWrong(id);
    // 正确的,线程安全
    Integer orderCount = seckillEvolutionService.createOptimisticLockOrderWithRedisSafe(id);
    return new ResponseBean(HttpStatus.OK.value(), "购买成功", null);
}

使用JMeter测试上面的代码,JMeter的使用可以查看: JMeter的安装使用

我们调用一下商品库存初始化的方法,我使用的是PostMan,初始化库存表商品10个库存,而且清空订单表

图片

接着使用PostMan调用缓存预热方法,提前加载好缓存

图片

这时候可以看到我们的数据,库存为10,卖出为0,订单表为空

图片

缓存数据也是这样

图片

打开JMeter,添加测试计划(测试计划文件在项目的src\main\resources\jmx下),模拟500个并发线程测试秒杀10个库存的商品,填写请求地址,点击启动图标开始

图片
图片

可以看到500个并发线程执行完,可以看到这次是正确的了

图片

日志也没问题了

logger.info("版本号:{} {} {}", stockDto.getCount(), stockDto.getSale(), stockDto.getVersion());
2019-11-23 13:08:48.494  INFO --- [nio-8080-exec-8] : 版本号:10 0 0
2019-11-23 13:08:48.587  INFO --- [nio-8080-exec-6] : 版本号:9 1 1
2019-11-23 13:08:48.691  INFO --- [io-8080-exec-13] : 版本号:8 2 2
2019-11-23 13:08:48.734  INFO --- [o-8080-exec-134] : 版本号:7 3 3
2019-11-23 13:08:48.778  INFO --- [io-8080-exec-29] : 版本号:6 4 4
2019-11-23 13:08:48.826  INFO --- [io-8080-exec-75] : 版本号:5 5 5
2019-11-23 13:08:48.874  INFO --- [io-8080-exec-30] : 版本号:4 6 6
2019-11-23 13:08:48.927  INFO --- [o-8080-exec-113] : 版本号:3 7 7
2019-11-23 13:08:48.974  INFO --- [o-8080-exec-180] : 版本号:2 8 8
2019-11-23 13:08:49.020  INFO --- [o-8080-exec-199] : 版本号:1 9 9

4.2. 结果总结

最后我们可以看下Druid的监控,地址: http://localhost:8080/druid/sql.html

使用了缓存,可以看到库存查询SQL只执行了一次,就是缓存预热那执行了一次,不像之前每次库存都去查数据库

图片

不过乐观锁更新操作还是执行了157SQL,遵从最后落地到数据库的请求数要尽量少的原则,有没有办法优化这里呢,可以的,实际上很多都是无效请求,这里我们可以使用限流,把大部分无效请求拦截了,尽可能保证最终到达数据库的都是有效请求

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345