商城项目-秒杀功能

这次完善项目的时候加入了秒杀功能。这个功能要考虑的地方挺多的,我在网上找了一些资料,然后把这个功能大致完成了。但是还有很多地方我没考虑到,有些地方实现的不是很好,等以后再回头看对这个功能进行进一步的完善吧。

记录一下我现在实现的一些功能:

redis做缓存:

因为秒杀这个功能并发性是很大的,所以如果在秒杀的时候直接对mysql数据库进行操作,数据库可能承受不住,所以一般情况下都会用redis做缓存,将秒杀商品的信息存到redis里面,当redis里面查不到数据的时候再进入mysql查询,如果查询到数据,就将mysql中的数据写入到redis。

商城项目-秒杀功能_第1张图片

库存问题:

在扣减内存时,要先判断一个用户是否已经购买过,确定未购买再进行扣减内存。但因为查询数据库和更新数据库不是原子性操作,在并发性很高的情况下,可能会出现超卖的情况。这时候可以使用lua脚本,查询和扣减内存的操作均在lua中进行,可以保证原子性。

key[1]:用来存储已经进行秒杀过的用户id

key[2]:存储秒杀商品内存

ARGV[1]:用户id

ARGV[2]:订单信息类转化为的Json

local userKey=KEYS[1];
local stockKey=KEYS[2];
local userId=ARGV[1];
local orderMsg=ARGV[2];
local userExists=redis.call('sismember',userKey,userId)
if(tonumber(userExists)==1) then
    return -2;
end;
if (redis.call('exists', stockKey) == 1) then
    local stock = tonumber(redis.call('get',stockKey));
    if (stock > 0) then
        redis.call('incrby', stockKey, -1);
        redis.call('sadd',userKey,userId);
        redis.call('lpush','orderList',orderMsg);
        return 1;
    end;
    return 0;
end;
return -1;

-2:用户已经秒杀过,不能再进行秒杀

-1:秒杀商品在redis里面不存在,返回后对数据库进行查询,如果商品存在将商品信息存入redis,不存在直接返回

0:库存不足,直接返回

1:秒杀成功,将库存减一,用户id存入已经秒杀过的用户中

缓存击穿:

在秒杀时先从redis数据库中查询数据,如果数据不存在就进入mysql查询。但是在高并发的情况下,一瞬间有很多请求同时进行,此时在redis中查询不到数据,就会全部进入mysql查询,会造成缓存击穿。解决缓存击穿有好几种方案,例如:热数据设置永不过期、加分布式锁。这里我使用了分布式锁,就是让很多请求同时查询不到数据的时候,只允许一个请求对mysql进行操作,这个请求将mysql中的数据读取到redis之后,其他请求再进入redis进行查询。

while(result.equals(MagicIntegerEnum.STOCK_NO_EXIST.getKey())){
    String lockKey="killLock";
    result = luaUtil.runLuaScript("Stock.lua",keyList,userId.toString(),orderJson);
    String userValue = UUID.randomUUID().toString();
    boolean lock = redisUtil.setNx(lockKey, userValue, 30, TimeUnit.SECONDS);
    if (lock) {
        SecKill kill = killDao.getByProId(proId, 1);
        List list=new ArrayList<>();
        list.add(lockKey);
        if(kill==null){
            luaUtil.runLockLua("Lock.lua",list,userValue);
            return ReturnUtil.error("无秒杀商品");
        }
        int size=0;
        if(redisUtil.hasKey(userKey)) {
            size = redisUtil.setMembers(userKey).size();
        }
        int stock=kill.getKillStock()-size;
        redisUtil.set(killKey, Integer.toString(stock));
        luaUtil.runLockLua("Lock.lua",list,userId.toString());
    }else {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
/**
*只有在 key 不存在时设置 key 的值,将值 value 关联到 key ,设置过期时间
*/
public boolean setNx(String key,String value,long timeout,TimeUnit unit){
    return redisTemplate.opsForValue().setIfAbsent(key, value, timeout, unit);
}

设置过期时间,防止死锁。

如果在进行删除操作之前,一个锁刚好到过期时间,此时另一个请求正好加锁,就会释放掉别人的锁,所以加锁的时候要加userValue,在释放锁的时候判断一下是否是自己的锁,如果是,再将锁释放。因为在get和del之间可能也会出现异常,所以也要保证原子性,就在使用了lua脚本。

local lockKey=KEYS[1];
local userValue=ARGV[1];
local exist=redis.call('exists', lockKey);
if(tonumber(exist)==0) then
    return 0;
end;
if(redis.call('get',lockKey)==userValue) then
    return tonumber(redis.call('del',lockKey));
end;
return 1;

异步下单:

在秒杀之后下单的操作其实并发性并没有那么高,我在网上找了一些资料,发现很多时候秒杀下单的时候会采用mq消息队列进行异步处理。但是因为我对这个不是很了解,就使用了redis的list做消息队列,这一部分完成的不是很理想,因为目前我对线程这一块的知识了解的特别特别特别模糊,等以后我学习了更多知识后再对这一部分进行完善。

目前的代码:

@PostConstruct
    private void init(){
        SKILL_ORDER.submit(new OrderHandle());
    }

    public class OrderHandle implements Runnable{
        @Override
        public void run() {
            String queue="orderList";
            while (true) {
                try {
                    String orderJson = redisUtil.lBRightPop(queue, 1, TimeUnit.MINUTES);
                    if (orderJson == null || "".equals(orderJson)) {
                        continue;
                    }
                    Order order = JSON.parseObject(orderJson, Order.class);
                    int i = (int) ((Math.random() * 9 + 1) * 100000);
                    LocalDateTime localDateTime = LocalDateTime.now();
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMss");
                    String format = localDateTime.format(formatter);
                    String code = format + order.getUserId() + order.getProId() + order.getAddId() + i;
                    SecKill kill = killDao.getByProId(order.getProId(), 1);
                    order.setOrderCode(code);
                    order.setOrderPrice(kill.getKillPrice());
                    orderDao.insertOrder(order);
                    String orderKey="killOrder:"+order.getOrderId();
                    redisUtil.setEx(orderKey,"1",10,TimeUnit.MINUTES);
                }
                catch (Exception e){
//                    e.printStackTrace();
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ex) {
                        ex.printStackTrace();
                    }
                    break;
                }
            }
        }
    }

@PostConstruct:在类启动的时候运行。

构造方法 ——> @Autowired —— > @PostConstruct ——> 静态方法 (按此顺序加载)

你可能感兴趣的:(redis,lua,数据库)