0%

Go+Redis缓存控制商品数量防止超卖《基础实现版》

首先了解使用Redis的思路

  1. 在秒杀商品页面,使用Redis缓存数据库中当前商品数量
  2. 运用Redis的DECR原子语句,减少存储value的值
  3. 在value>=0时,使用MQ消息队列异步处理下单,数据库库存减少操作
  4. value<0后返回False,不执行下单操作,缓解数据库压力

Go+Redis代码实现思路

  1. 首先使用Go语言第三方库github.com/garyburd/redigo/redis,建立redis连接池

为什么要建立redis连接池?

redis连接方式是客户端/服务端的TCP连接,多次使用redis直接连接,会导致可能redis连接的时间比处理数据的时间还长,

效率不高,这并不是我们想要的,通过建立连接池的方式,想连接就使用,不想连接,就放回连接池,极大提高redis使用效率。

  1. 在商品页面缓存商品数量
  2. 秒杀接口扣除商品数量防止超卖

连接池代码如下

package common

import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	"time"
)

var (
	pool *redis.Pool
	//使用红帽 ACL 系统连接到红帽实例
	redisHost = "127.0.0.1:6379"
	//redisHost = ":6379"
	//密码
	redisPass = ""
)

// newRedisPool : 创建redis连接池
func newRedisPool() *redis.Pool {
	return &redis.Pool{
		MaxIdle:     50,
		MaxActive:   0,
		IdleTimeout: 300 * time.Second,
		Dial: func() (redis.Conn, error) {
			// 1. 打开连接
			c, err := redis.Dial("tcp", redisHost)
			if err != nil {
				fmt.Println(err)
				return nil, err
			}

			// 2. 访问认证
			//if _, err = c.Do("AUTH", redisPass); err != nil {
			//	c.Close()
			//	return nil, err
			//}
			if len(redisPass) > 0 { // 有密码的情况
				if _, err := c.Do("AUTH", redisPass); err != nil {
					c.Close()
					return nil, err
				}
			} else { // 没有密码的时候 ping 连接
				if _, err := c.Do("ping"); err != nil {
					c.Close()
					return nil, err
				}
			}
			return c, nil
		},
		//检测Redis是否可用
		TestOnBorrow: func(conn redis.Conn, t time.Time) error {
			if time.Since(t) < time.Minute {
				return nil
			}
			_, err := conn.Do("PING")
			return err
		},
	}
}

func init() {
	pool = newRedisPool()
}

func RedisPool() *redis.Pool {
	return pool
}

缓存商品数量

// GetDetailr 秒杀页面/商品详情页 需要用户登录才能看 curl //detailr--------Redis
func (p *ProductController) GetDetailr() mvc.View {
	//固定产品测试
	product, err := p.ProductService.GetProductByID(4)
	if err != nil {
		p.Ctx.Application().Logger().Debug(err)
	}
	//fmt.Println("-----------产品路径------------", product)
	//p.Ctx.Redirect("/product/Localnum")
	//redis 缓存商品数量
	rConn := common.RedisPool().Get()
	defer rConn.Close()
	_, err = rConn.Do("SET", "Count", product.ProductNum)
	if err != nil {
		log.Fatal("set字符串失败,", err)
	}
	return mvc.View{
		//商品详情布局文件
		Layout: "shared/productLayout.html",
		//页面模板
		Name: "product/view.html",
		Data: iris.Map{
			"product": product,
		},
	}
}

秒杀接口


// GetQrder 测试接口 改进前 使用Redis
// go-wrk -c 80 -d 5 http://localhost:8082/product/rrder
func (p *ProductController) GetRrder() []byte {
	//通过消息队列 缓解订单更新数据库压力
	//productIDString转换成int64
	productID := int64(4)

	userID := int64(5)
	//连接redis
	rConn := common.RedisPool().Get()
	defer rConn.Close()
	if val, _ := redis.Int(rConn.Do("DECR", "Count")); val >= 0 {
		//创建消息体
		message := datamodels.NewMessage(productID, userID)
		//类型转化 成rabbitmq可以传送的消息
		byteMessage, err := json.Marshal(message)
		fmt.Println()
		if err != nil {
			p.Ctx.Application().Logger().Debug(err)
		}
		//使用Simple模式消息队列 实例定义好的模式
		err = p.RabbitMQ.PublishSimple(string(byteMessage))
		if err != nil {
			p.Ctx.Application().Logger().Debug(err)
		}
		fmt.Println("消费ing")
		return []byte("true")
	} else {
		fmt.Println("卖完了")
		return []byte("false")
	}
}

MQ消息队列

消息队列消费微服务
package main

import (
	"fmt"
	"imooc-Product/common"
	"imooc-Product/rabbitmq"
	"imooc-Product/repositories"
	"imooc-Product/services"
)

/*
	消费端代码
*/
func main() {
	//获取数据库实例
	db, err := common.NewMysqlConn()
	if err != nil {
		fmt.Println(err)
	}
	//创建product数据库操作实例
	product := repositories.NewProductManager("product", db)
	//创建 product service
	productService := services.NewProductService(product)
	//创建order 数据库操作实例
	order := repositories.NewOrderManager("order", db)
	//创建 order service
	orderService := services.NewOrderService(order)
	//消息队列消费者
	rabbitmqConsumer := rabbitmq.NewRabbitMQSimple("imoocProduct")
	//进行消费
	rabbitmqConsumer.ConsumeSimple(orderService, productService)

}
MQ订单消费
// ConsumeSimple  ----------step:3. 简单模式下消费者
func (r *RabbitMQ) ConsumeSimple(orderService services.IOrderService, productService services.IProductService) {
   //1. 申请队列,如果队列不存在会自动创建,如果存在则跳过创建(保证队列存在,消息能发送到队列中)
   _, err := r.channel.QueueDeclare(
      r.QueueName,
      //是否持久化
      false,
      //是否自动删除
      false,
      //是否具有排他性
      false,
      //是否阻塞
      false,
      //额外属性
      nil,
   )
   if err != nil {
      fmt.Println(err)
   }
   //消费者控流
   r.channel.Qos(
      1,     //当前消费者一次能接受最大消息数量1个
      0,     //服务器传递的最大容量()以8位字节为单位
      false, //如果为true 对channel可用
   )

   //2. 接受消息
   msgs, err := r.channel.Consume(
      r.QueueName,
      //用来区分多个消费者
      "",
      //是否自动应答
      //可改手动修改【false】 控制速率
      false,
      //是否独有
      false,
      //如果设置为true,表示不能将同一个connection中发送的消息传递给这个connection中的消费者
      false,
      //是否阻塞 false为阻塞
      false,
      nil)
   if err != nil {
      fmt.Println(err)
   }
   forever := make(chan bool)
   //启用协程处理消息
   var num int
   go func() {
      for d := range msgs {

         //实现我们要处理的逻辑函数
         //log.Printf("Received a message:%s", d.Body)
         message := &datamodels.Message{}
         err := json.Unmarshal(d.Body, message)
         if err != nil {
            fmt.Println(err)
         }
         num++
         fmt.Println("Received a message:", string(d.Body), "第:", num)
         //数据库插入订单
         _, err = orderService.InsertOrderByMessage(message)
         if err != nil {
            fmt.Println(err)
         }

         //
         err = productService.SubNumberOne(message.ProductID)
         if err != nil {
            fmt.Println(err)
         }
         //如果为true 表示确认所有未确认的消息
         //为false 表示确认当前消息
         d.Ack(false) //手动确认消息 正确才下一个 保障数据不丢失
      }
   }()

   log.Printf("[*] Waiting for message,To exit press CTRL+C")
   //forever里面没有元素,使用这种方法让协程保持阻塞 这样主程序不会死掉
   <-forever
}

代码中的重点

  1. Redis端

数量判断必须与该数量减少原子语句操作在同一语句,否则高并发下仍然会发生超卖现象

if val, _ := redis.Int(rConn.Do("DECR", "Count")); val >= 0 {

}else{

}
  1. MySQL端

为避免高并发下超卖,更新操作在SQL中使用数量减少的原子语句

sql := "update " + p.table + " set " + " productNum=productNum-1 where ID=" 
-------------本文结束感谢您的阅读-------------
打赏一瓶矿泉水