首先了解使用Redis的思路
- 在秒杀商品页面,使用Redis缓存数据库中当前商品数量
- 运用Redis的
DECR
原子语句,减少存储value的值 - 在value>=0时,使用MQ消息队列异步处理下单,数据库库存减少操作
- value<0后返回False,不执行下单操作,缓解数据库压力
Go+Redis代码实现思路
- 首先使用Go语言第三方库
github.com/garyburd/redigo/redis
,建立redis连接池
为什么要建立redis连接池?
redis连接方式是客户端/服务端的TCP连接,多次使用redis直接连接,会导致可能redis连接的时间比处理数据的时间还长,
效率不高,这并不是我们想要的,通过建立连接池的方式,想连接就使用,不想连接,就放回连接池,极大提高redis使用效率。
- 在商品页面缓存商品数量
- 秒杀接口扣除商品数量防止超卖
连接池代码如下
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
}
代码中的重点
- Redis端
数量判断必须与该数量减少原子语句操作在同一语句,否则高并发下仍然会发生超卖现象
if val, _ := redis.Int(rConn.Do("DECR", "Count")); val >= 0 {
}else{
}
- MySQL端
为避免高并发下超卖,更新操作在SQL中使用数量减少的原子语句
sql := "update " + p.table + " set " + " productNum=productNum-1 where ID="