暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

redis扣减商品库寸

原创 手机用户7109 2025-10-21
85

数据设计:

num:offset(offset是zet里的分数),skuID%50是sku所在的zset,会有线程定时消费zset里的数据,采用Lua脚本保证原子性:
lua步骤:
1.查询sku,版本号,版本号大于zset标识已经消费过了,跳过消费
2.执行更换,
3.删除zset里的数据
定时消费逻辑:
1.redis获取分布式锁,
2.从redis zset里获取数据
3.执行lua脚本回单
分布式锁因为是消费redis里的数据,所以每次执行lua脚本前都需要检测锁是否还在,不在退出执行,分布式锁设置成永久锁.watchDog定时续期

商品扣减逻辑:
由于回滚的需要,按照按照队列进行扣减.采用多线程部署的方式,设置一个timeout和一个ConcurrentMap记录扣减结果,超时就表示扣减失败;重新提交订单;
扣减库存的lua:

-- 查询商品数量,不够就不扣了 -- 查询sku对应的zset的成员分数值,对该值增1,失败就重试三次(hashmap结构,hget) -- 更新zset分数值 -- 写zset -- 更新sku值. -- 查询redisId,返回 实现为: local skuNumNeed=tonumber(ARGV[1]) or 0 local skuId=KEYS[1] local sku_redo_log_queue=KEYS[2] local orderItemId=ARGV[2] -- 查询商品数量,不够就不扣了 local numStr=redis.call('hget','sku',skuId) local key, value = string.match(numStr, "([^:]+):([^:]+)") -- 如果值是数字,可以转换 value = tonumber(value) or value if value < skuNumNeed then return "" end -- 查询sku对应的zset的成员分数值,对该值增1,失败就重试三次(hashmap结构,hget) local offset=redis.call('hget','queueOffset',sku_redo_log_queue) -- 更新zset分数值 local numOffset = tonumber(offset) or 0 local newOffset=numOffset+1 redis.call('hset','queueOffset',sku_redo_log_queue,newOffset) -- 写zset redis.call('zadd',sku_redo_log_queue, offset,orderItemId) --更新商品 local skuNewNum=value-skuNumNeed local skuNew=skuId .. tostring(skuNewNum) redis.call('hset','sku',skuId,skuNew) -- 查询redisId,返回 local redisId=redis.call('hget','metadata','redis_id') if redisId==nil or redisId=="" then return "" end return redisId
该类用于在多线程间传递结果.
class MultiSubmitOrderWEntity{
	Map<String,List<>> map		记录扣减成功的数据
	AtomicInteger    status; 用于在多线程间传递结果.
	countDownLatch   主线程阻塞.
}

扣减主线程代码:

OrderVo order; orderMap=new MultiSubmitOrderMap(); latch=orderMap.getLatch(); //按照redis分片的逻辑(也是mysql分片的逻辑),将商品进行分类; List<List<OrderItem>> list=order.getList(); for int i=0;i<list.size();i++{ submit(orderMap,list.get(i)); //提交至线程池 } latch.countDown(list.size(),500ms); //判断扣减状态, //失败就回滚 //成功需要写消息队列.

扣减线程代码: OrderItemStockRedisRun

for循环扣减队列里的商品 //检查是否有扣减失败的,有就终止扣减,退出 //扣减 查询数量,不够continue lua脚本扣减 返回redisID latch.countDown();

扣减失败就需要回滚,

local skuID=KEYS[1] local skuNum=ARGV[1] local numStr=redis.call('hget','sku',skuID) local key, value = string.match(numStr, "([^:]+):([^:]+)") -- 如果值是数字,可以转换 value = tonumber(value) or value --更新商品 local skuNewNum=value+skuNum local skuNew=skuId .. tostring(skuNewNum) redis.call('hset','sku',skuId,skuNew) return 0

商品扣减过程中路由是一个问题,不同的skuId需要路由到不同的redis上,这里使用Dubbo构建redis代理.
实际的商品扣减

超时回单:
1.mysql:利用消息队列,超时回单
2.redis:利用redis本身的队列,超时回单.
20bit 50 50 000 50 000 000
1 10 10 000 10 000 000
100bit 1k 1M 1G

10个队列,10G,管理1亿个订单项
9 223 372 036 854 775 807 17位
orderItemId: skuId: skuNum
17 17 4
从redoLog角度看只需要存储orderItemId就行了,到时都是要查数据库的,所以数据大小按照50字节来估算,10G可以存储2亿条订单项.
redis代理实现:
通过Dubbo,connectionFactory两个层面实现redis代理
1.dubbo过渡不同的业务
2.connectionFactory区分同一个业务的不同数据源
连接池的实现目前有Jedis,Lettuce两个,Lettuce的性能远好于Jedis,因此本次使用Lettuce实现多数据源
使用ConcurrentHashMap保存redisTemplate;
Map<String,Map<String,StringRedisTemplate>> redisTemplateMap=new ConcurrentHashMap();
第一个String是业务区分,第二个是ip区分,用于删除,添加redisTemplate
在某个方法里统一创建好所有的redisTemplate;使用时获取即可.

最后修改时间:2025-10-27 01:41:37
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论