5.9 Go语言项目实战:驾考系统

项目需求

  • 考场签到,名字丢入管道;
  • 只有5个车道,最多供5个人同时考试;
  • 考生按签到顺序依次考试,给予考生10%的违规几率;
  • 每3秒钟巡视一次,发现违规的清出考场,否则输出考场时序良好;
  • 所有考试者考完后,向MySQL数据库录入考试成绩;
  • 成绩录入完毕通知考生,考生查阅自己的成绩;
  • 当前目录下的成绩录入MySQL数据库,数据库允许一写多读;
  • 再次查询成绩使用Redis缓存(二级缓存);
  • 整理优化代码,提高复用程度;

主要技术栈

  • 管道并发
  • MySQL-Redis二级缓存
  • 通用的数据库工具的封装
  • 类库封装和复用

错误处理工具

  • 主要用于输出错误与错误爆发的场景,并暴力退出程序(当然是在DEBUG模式下)
package utils

import (
	"fmt"
	"os"
)

/*处理错误:有错误时暴力退出*/
func HandlerError(err error, when string) {
	if err != nil {
		fmt.Println(when, err)
		os.Exit(1)
	}
}

数学工具

  • 这里主要封装了一个获取指定范围内随机数的方法
  • 考虑到任意两次调用间时间种子不得相同,我们使用了强制同步和阻塞睡眠
package utils

import (
	"time"
	"math/rand"
	"sync"
)

var(
	//随机数互斥锁(确保GetRandomInt不能被并发访问)
	randomMutex sync.Mutex
)

/*获取[start,end]范围内的随机数*/
func GetRandomInt(start, end int) int {
	randomMutex.Lock()
	<-time.After(1 * time.Nanosecond)
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	n := start + r.Intn(end-start+1)
	randomMutex.Unlock()
	return n
}

起名系统

  • 详见:起名系统详解
package utils

var (
	//姓氏
	familyNames = []string{"赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈", "楚", "卫", "蒋", "沈", "韩", "杨", "张", "欧阳", "东门", "西门", "上官", "诸葛", "司徒", "司空", "夏侯"}
	//辈分(宗的永其光...)
	middleNamesMap = map[string][]string{}
	//名字
	lastNames = []string{"春", "夏", "秋", "冬", "风", "霜", "雨", "雪", "木", "禾", "米", "竹", "山", "石", "田", "土", "福", "禄", "寿", "喜", "文", "武", "才", "华"}
)

/*初始化姓氏和对应的辈分*/
func init() {
	for _, x := range familyNames {
		if x != "欧阳" {
			middleNamesMap[x] = []string{"德", "惟", "守", "世", "令", "子", "伯", "师", "希", "与", "孟", "由", "宜", "顺", "元", "允", "宗", "仲", "士", "不", "善", "汝", "崇", "必", "良", "友", "季", "同"}
		} else {
			middleNamesMap[x] = []string{"宗", "的", "永", "其", "光"}
		}
	}
}

/*获得随机姓名*/
func GetRandomName() (name string) {
	familyName := familyNames[GetRandomInt(0, len(familyNames)-1)]
	middleName := middleNamesMap[familyName][GetRandomInt(0, len(middleNamesMap[familyName])-1)]
	lastName := lastNames[GetRandomInt(0, len(lastNames)-1)]
	return familyName + middleName + lastName
}

数据模型

  • 主要记录学生的姓名和考试成绩
package utils

/*考试成绩*/
type ExamScore struct {
	Id    int    `db:"id"`
	Name  string `db:"name"`
	Score int    `db:"score"`
}

数据库工具

这份代码实现了二级缓存的核心功能,包括:

  • 将全员考试成绩单(姓名/成绩键值对map)写入MySQL数据库
  • 根据姓名从Redis缓存查询分数
  • 将姓名与分数写入Redis缓存
  • 还实现了一个通用的MySQL表查询方法:根据任意表名和查询条件map进行查询,并将结果送入指定的指针地址中,从而具有较高的复用价值
package utils

import (
	"fmt"
	/*MySQL*/
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	"github.com/garyburd/redigo/redis"
	"errors"
	"sync"
)

var(
	//数据库读写锁
	dbMutext sync.RWMutex
)

/*
通用Mysql查询工具
tableName	要查询的表名
argsMap		查询条件集合
dest		查询结果存储地址
*/
func QueryFromMysql(tableName string,argsMap map[string]interface{},dest interface{}) (err error) {
	fmt.Println("QueryScoreFromMysql...")

	//写入期间不能进行数据库读访问
	dbMutext.RLock()
	db, err := sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")
	HandlerError(err, `sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")`)
	defer db.Close()

	selection := ""
	values := make([]interface{}, 0)
	for col,value := range argsMap{
		selection += (" and "+col+"=?")
		values = append(values, value)
	}
	selection = selection[4:]
	sql := "select * from "+tableName+" where "+selection;

	err = db.Select(dest, sql, values...)
	if err != nil {
		fmt.Println(err, `db.Select(&examScores, "select * from score where name=?;", name)`)
		return
	}

	dbMutext.RUnlock()
	return nil
}

/*将全员考试成绩单写入MySQL数据库*/
func WriteScore2Mysql(scoreMap map[string]int) {
	//锁定为写模式,写入期间不允许读访问
	dbMutext.Lock()
	db, err := sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")
	HandlerError(err, `sqlx.Connect("mysql", "root:123456@tcp(localhost:3306)/driving_exam")`)
	defer db.Close()

	for name, score := range scoreMap {

		_, err := db.Exec("insert into score(name,score) values(?,?);", name, score)
		HandlerError(err, `db.Exec("insert into score(name,score) values(?,?);", name, score)`)
		fmt.Println("插入成功!")
	}
	fmt.Println("成绩录入完毕!")

	//解锁数据库,开放查询
	dbMutext.Unlock()
}

/*根据姓名从Redis缓存查询分数*/
func QueryScoreFromRedis(name string) (score int, err error) {
	fmt.Println("QueryScoreFromRedis...")
	conn, err := redis.Dial("tcp", "localhost:6379")
	HandlerError(err, `redis.Dial("tcp", "local:6379")`)
	defer conn.Close()

	reply, e := conn.Do("get", name)
	if reply != nil {
		score, e = redis.Int(reply, e)
		//fmt.Println("!!!!!!!!!!!!", score, e)
	} else {
		return 0, errors.New("未能从Redis中查到数据")
	}

	if err != nil {
		fmt.Println(err, `conn.Do("get", name)或者redis.Int(reply, err)`)
		return 0, e
	}

	return score, nil
}

/*将姓名与分数写入Redis缓存*/
func WriteScore2Redis(name string, score int) error {
	conn, err := redis.Dial("tcp", "localhost:6379")
	HandlerError(err, `redis.Dial("tcp", "local:6379")`)
	defer conn.Close()

	_, err = conn.Do("set", name, score)
	fmt.Println("Redis写入成功!")
	return err
}

考试业务封装

  • 这里封装了巡考、考试、查成绩逻辑
package main

import (
	"time"
	"fmt"
	"utils"
)

var(
	chNames = make(chan string, 100)
	examers = make([]string, 0)

	//信号量,只有5条车道
	chLanes = make(chan int, 5)
	//违纪者
	chFouls = make(chan string, 100)

	//考试成绩
	scoreMap = make(map[string]int)
)

/*巡考逻辑*/
func Patrol() {
	ticker := time.NewTicker(1 * time.Second)
	for {
		//fmt.Println("战狼正在巡考...")
		select {
		case name := <-chFouls:
			fmt.Println(name, "考试违纪!!!!! ")
		default:
			fmt.Println("考场秩序良好")
		}
		<-ticker.C
	}
}

/*考试逻辑*/
func TakeExam(name string) {
	chLanes <- 123
	fmt.Println(name, "正在考试...")

	//记录参与考试的考生姓名
	examers = append(examers, name)

	//生成考试成绩
	score := utils.GetRandomInt(0, 100)
	//fmt.Println(score)
	scoreMap[name] = score
	if score < 10 {
		score = 0
		chFouls <- name
		//fmt.Println(name, "考试违纪!!!", score)
	}

	//考试持续5秒
	<-time.After(400 * time.Millisecond)

	<-chLanes
	//wg.Done()
}

/*二级缓存查询成绩*/
func QueryScore(name string) {
	score, err := utils.QueryScoreFromRedis(name)
	if err != nil {
		fmt.Println(err)
		//score, _ = utils.QueryScoreFromMysql(name)

		scores := make([]utils.ExamScore, 0)
		argsMap := make(map[string]interface{})
		argsMap["name"] = name
		//argsMap["score"] = 50
		err = utils.QueryFromMysql("score", argsMap, &scores)
		utils.HandlerError(err,`utils.QueryFromMysql("score", argsMap, &scores)`)
		fmt.Println("Mysql成绩:", name, ":", scores[0].Score)

		/*将数据写入Redis*/
		utils.WriteScore2Redis(name, scores[0].Score)

	} else {
		fmt.Println("Redis成绩:", name, ":", score)
	}
	//wg.Done()
}

主调程序

package main

import (
	"time"
	"fmt"
	"utils"
	"sync"
)

/*
考场签到,名字丢入管道;
只有5个车道,最多供5个人同时考试;
考生按签到顺序依次考试,给予考生10%的违规几率;
每3秒钟巡视一次,发现违规的清出考场,否则输出考场时序良好;
所有考试者考完后,向MySQL数据库录入考试成绩;
成绩录入完毕通知考生,考生查阅自己的成绩;
当前目录下的成绩录入MySQL数据库,数据库允许一写多读;
再次查询成绩使用Redis缓存(二级缓存);
整理优化代码,提高复用程度;
*/

var (
	wg sync.WaitGroup
)

/*主程序*/
func main() {
	for i := 0; i < 20; i++ {
		chNames <- utils.GetRandomName()
	}
	close(chNames)

	/*巡考*/
	go Patrol()

	/*考生并发考试*/
	for name := range chNames {
		wg.Add(1)
		go func(name string) {
			TakeExam(name)
			wg.Done()
		}(name)
	}

	wg.Wait()
	fmt.Println("考试完毕!")

	/*录入成绩*/
	wg.Add(1)
	go func() {
		utils.WriteScore2Mysql(scoreMap)
		wg.Done()
	}()
	//故意给一个时间间隔,确保WriteScore2DB先抢到数据库的读写锁
	<-time.After(1 * time.Second)

	/*考生查询成绩*/
	for _, name := range examers {
		wg.Add(1)
		go func(name string) {
			QueryScore(name)
			wg.Done()
		}(name)
	}
	<-time.After(1 * time.Second)
	for _, name := range examers {
		wg.Add(1)
		go func(name string) {
			QueryScore(name)
			wg.Done()
		}(name)
	}

	wg.Wait()
	fmt.Println("END")
}

你可能感兴趣的:(Go语言实战项目)