Redis与数据库保持一致(最小代价实现)

背景:
网上看了一大圈基本,网友的意见基本有以下方式:
1.先删缓存、再更新数据库
2.先更新数据库再删缓存
注:高并发下都无法保证数据的一致性,可保证最终一致性,有风险。
3.使用binlog+MQ工具(类似于mysql slave),截获取增量日志,可行但代价太高。由于多了一层,更增加了系统的复杂度和不稳定风险。
现想到如下方案供大家探讨可行性:
一、定义如下表:
在这里插入图片描述
1)商品入库时初始库存量为实际库存量,冻结数=0,缓存消耗数=0;
2)【总库存】计算方式=库存+(冻结数-缓存消耗数);
二、将商品库存放入Redis的步聚,以“牙刷”为例:
1)、获取牙刷的总库存
select 总库存=库存+冻结数-缓存消耗数 from 商品名称=牙刷
2)、更新数据库“牙刷”商品行
【冻结数】=【总库存】-【总库存】/2
update 库存=库存-【冻结数】,冻结数=冻结数+【冻结数】 where 商品名称=“牙刷” and (库存+【冻结数】-缓存消耗)>0
3)、如(2)成功,将【冻结数】加入Redis
RedisTempldate.put(“牙刷”,Long.MaxValue-【冻结数】)
4)还原”库存“操作
update 库存=库存+冻结-缓存消耗数**,冻结数=0,缓存消耗数=0 where 商品名称=“牙刷”
说明:用以上方式,省去"0"判断,当在”缓存“中减库存时,原子增量加**,加到我们放入缓存中的数量,将抛出异常,得用这个异常决定下一步操作,是删去”缓存“还是”从数据库中“更新缓存。**
小结:利用本地事务,和拆分”库存“的方式,我们实现了以下目标:
1、数据的更新利用本地事务解决啦。
2、利用拆分”库存“一分为二实现了在高并发情况下数据库表和缓存不一致的问题。

三、查询库存
1、如果”缓存当中”没有或(Long.MAXValue-Redis.get("牙刷”))<0
1)、从数据库当中判断【总库存】
2)、有则删除缓存“牙刷”。
四、下单减库存
1、如果该商品在缓存中
1)Redus.Incr 如果成功,将数据库中的【缓存消耗数】加下单商品数
2)如果报错(超过Long.MAXVALUE 阈值):还原库存见(二.4),再判断“库存”,将库存减-下单数量。
2、如果没在缓存中
数据库中 库存-下单数量

纵观整个流程 库存量有可能被减为负数,这是正常的。如果要严格一致 可以实时更新 库存量或缓存消耗数。
如果对“一致性”可以有一定程度上的容忍,可以使用MQ来更新“缓存消耗数”和库存。
这样,我们通过三个字段实现了库存的缓存和库存量的控制,既加快了查询,也解决了“库存”和缓存不一致的问题。又增加了技术实现“弹性”
总体效果:
在这里插入图片描述
四、模拟代码:

package com.acwer;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 将库存一分为二,放入缓存当中,解决常见数据库库存与缓存不一致问题
 * 说明:本程序只是一个模拟应用程序
 * 模拟用到的技术:Redis、CAS锁、安全HASHMAP
 * 实际要用到的框架:Redis HASH、数据库(MYSQL、MSSQL)等,MQ(消息队列)
 */
public class Redis2Db {

    public static void main(String[] args) {

        Redis2Db db = new Redis2Db();
        //TODO:(简单模拟)随机放入一部分到缓存
        //实际使有MQ时,应先尝试还原库存,再放入缓存
        for (Map.Entry<String, AtomicReference<商品库存>> entry : db.库存Table.entrySet()) {
            Random r = new Random();
            int cacheCount = 100+ r.nextInt(entry.getValue().get().库存 / 2);
            db.Redis2Count.put(entry.getKey(), new AtomicInteger(cacheCount));
            entry.getValue().get().库存 = entry.getValue().get().库存 - cacheCount;
            entry.getValue().get().冻结 = cacheCount;
        }

        db.列出商品列表();

        db.创建模拟订单();
        while (db.库存充足()) {
            new Thread(() -> {
               // sleep();
                db.创建模拟订单();
            }).start();

        }
        while (Thread.activeCount() > 2) {
            sleep();
        }

        db.列出商品列表();

    }

    static void sleep() {
        Random r = new Random();
        try {
            Thread.sleep(r.nextInt(100));
        } catch (Exception e) {
        }
    }

    //生成订单编号用到
    AtomicInteger orderGener = new AtomicInteger(1);

    public void 创建模拟订单() {
        int size = 库存Table.size();
        Random r = new Random();
        int loop = size + r.nextInt(size);
        Map<String, Integer> map = new HashMap<>();
        Enumeration<String> keys = 库存Table.keys();
        List<String> 商品名称s = new ArrayList<>();
        while (keys.hasMoreElements()) {
            商品名称s.add(keys.nextElement());
        }
        for (int i = 0; i < loop; i++) {
            String goodName = 商品名称s.get(r.nextInt(size));
            int count = 1 + r.nextInt(50);
            Integer orDefault = map.getOrDefault(goodName, 0);
            map.put(goodName, count + orDefault);
        }
        String orderNo = String.valueOf(Math.pow(10, 6) + orderGener.getAndIncrement());
        orderNo = orderNo.substring(1);
        下订单(map, orderNo);

    }

    public void 列出商品列表() {
        System.out.println("===商品库存====");
        for (Map.Entry<String, AtomicReference<商品库存>> entry : 库存Table.entrySet()) {
            String gn = entry.getKey();
            System.out.println(gn + "=>" + entry.getValue().get());
        }
    }

    public void 下订单(Map<String, Integer> goods, String orderNo) {
        System.out.println("===订单:" + orderNo + "=====");
        for (Map.Entry<String, Integer> entry : goods.entrySet()) {
            boolean success = 扣减库存(entry.getKey(), entry.getValue());
            if (success) {
                System.out.println(entry.getKey() + ":" + entry.getValue() + ",下单成功");
            } else {
                System.out.println(entry.getKey() + ":" + entry.getValue() + ",下单失败");
            }
        }
    }

    public void 还原库存(String good) {
        //实际使用时MQ消息
//        new Thread(()->{
            AtomicReference<商品库存> 商品库存Reference = 库存Table.get(good);
            //使用CAS模拟数据库事务、原子
            for (; ; ) {
                商品库存 商品库存 = 商品库存Reference.get();
                if (商品库存.冻结 == 0) {
                    return;
                }
                if (商品库存.冻结.intValue() == 商品库存.消耗.intValue()) {
                    商品库存 n库存 = new 商品库存(商品库存.库存, 0, 0);
                    if (商品库存Reference.compareAndSet(商品库存, n库存)) {
                        break;
                    }
                } else break;
            }

//        }).start();
    }

    public void 累加消耗(String good, Integer count) {
        AtomicReference<商品库存> 商品库存Reference = 库存Table.get(good);
        for (; ; ) {
            商品库存 商品库存 = 商品库存Reference.get();
            商品库存 n库存 = new 商品库存(商品库存.库存, 商品库存.冻结, 商品库存.消耗 + count);
            if (商品库存Reference.compareAndSet(商品库存, n库存)) {
                break;
            }
        }
    }

    ConcurrentHashMap<String, AtomicInteger> Redis2Count = new ConcurrentHashMap<>();

    public boolean 扣减库存(String goodName, Integer count) {
        AtomicInteger cacheCounter = null;

        for (; (cacheCounter = Redis2Count.get(goodName)) != null; ) {
            Integer cacheCount = cacheCounter.get();
            if (cacheCount >= count) {
                //实际使用当中用Redis incr
                if (cacheCounter.compareAndSet(cacheCount, cacheCount - count)) {
                    //模拟MQ消息更新缓存消耗
//                    new Thread(()->{
                        累加消耗(goodName, count);
//                    }).start();
                    return true;
                }
            } else {
                //如果缓存当中的数量不足以扣减
                AtomicInteger remove = Redis2Count.remove(goodName);
                //将当前剩余的数量加到数据库并修改冻结列的值
                redis2db(goodName, remove.get());
                break;
            }
        }
        return db扣减库存(goodName, count);
    }

    /**
     * 将缓存当中的商品数量移入数据库库存
     *
     * @param good  商品名称
     * @param count 移入数量
     */
    private void redis2db(String good, Integer count) {
        AtomicReference<商品库存> 商品库存Reference = 库存Table.get(good);
        for (; ; ) {
            商品库存 库存 = 商品库存Reference.get();

            Integer 冻结数 = 库存.冻结 - count;

            商品库存 n库存 = new 商品库存(库存.库存 + count, 冻结数, 库存.消耗);
            if (商品库存Reference.compareAndSet(库存, n库存)) {
                break;
            }
        }
        还原库存(good);
    }

    /**
     * 数据库当中扣减库存
     *
     * @param goodName
     * @param count    扣减成功返回True,库存不足返回 false
     */
    private boolean db扣减库存(String goodName, Integer count) {

        AtomicReference<商品库存> 商品库存Reference = 库存Table.get(goodName);
        //原子更新库存
        for (; ; ) {
            商品库存 商品库存 = 商品库存Reference.get();
            if (商品库存.库存 >= count) {
                商品库存 n库存 = new 商品库存(商品库存.库存 - count, 商品库存.冻结, 商品库存.消耗);
                if (商品库存Reference.compareAndSet(商品库存, n库存)) {
                    return true;
                }
            } else break;
        }
        return false;

    }

    static class 商品库存 {
        public Integer 库存;
        public Integer 冻结;
        public Integer 消耗;

        public 商品库存(Integer a, Integer b, Integer c) {
            库存 = a;
            冻结 = b;
            消耗 = c;
        }


        public Integer 总库存() {
            int total = 库存 + 冻结;
            return total;
        }

        @Override
        public String toString() {
            return "库存=" + 库存 +
                    ", 冻结=" + 冻结 +
                    ", 消耗=" + 消耗;
        }
    }

    public boolean 库存充足() {
        return 库存Table.entrySet().stream().map(it -> {
            商品库存 商品库存 = it.getValue().get();
            return 商品库存.库存 + 商品库存.冻结 - 商品库存.消耗;
        }).filter(it -> it > 0).count() > 0;
    }

    //商品库存表
    ConcurrentHashMap<String, AtomicReference<商品库存>> 库存Table = new ConcurrentHashMap<String, AtomicReference<商品库存>>() {
        {
            put("牙刷", new AtomicReference<>(new 商品库存(1000, 0, 0)));
            put("手表", new AtomicReference<>(new 商品库存(2000, 0, 0)));
            put("手机", new AtomicReference<>(new 商品库存(3000, 0, 0)));
            put("袜子", new AtomicReference<>(new 商品库存(5000, 0, 0)));
            put("皮鞋", new AtomicReference<>(new 商品库存(10000, 0, 0)));
        }
    };

}


输出结果(太多,中间有删减)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值