loraserver 源码解析 (五) loraserver

7 篇文章 6 订阅

loraserver 是 LoRa Wan   networkserver (简称 ns)的核心

 

loraserver 连接 lora-app-server的 grpc 由 asclient 负责

asclient 写了个连接池

type client struct {
	client     as.ApplicationServerServiceClient
	clientConn *grpc.ClientConn
	caCert     []byte
	tlsCert    []byte
	tlsKey     []byte
}

type pool struct {
	sync.RWMutex
	clients map[string]client
}

其中 hostname:ip 组成key值, grpc dial成功后连接保存在 client 中 

 

lora-app-server 开启了 joinServer http 接口

server="http://localhost:8003"

loraserver 通过  jsclient 包与lora-app-server交互 来处理 JoinServer相关的请求

 

networkController 网络控制

 

loraserver 对外 grpc api 服务

  bind="0.0.0.0:8000"

和普通的grpc代码差不多,开启个tcp端口侦听,然后注册grpc  NetworkServerService 服务

 

从main函数开始

loraserver 也是基于 cobra 完成命令行的处理

作者搞了个 root_run.go 单独存放 run 函数

run 函数 是整个 loraserver 的核心

 

setGatewayBackend

为方便后续文章编写,config.C.NetworkServer.Gateway.Backend.Backend 简称  GatewayBackend

      GatewayBackend 在setGatewayBackend() 中完成创建

func setGatewayBackend() error {
	gw, err := gwBackend.NewMQTTBackend(
		config.C.Redis.Pool,
		config.C.NetworkServer.Gateway.Backend.MQTT,
	)
	if err != nil {
		return errors.Wrap(err, "gateway-backend setup failed")
	}
	config.C.NetworkServer.Gateway.Backend.Backend = gw
	return nil
}

 GatewayBackend 订阅了 uplink_topic_template  和  stats_topic_template 2个主题

  uplink_topic_template="gateway/+/rx"
  downlink_topic_template="gateway/{{ .MAC }}/tx"
  stats_topic_template="gateway/+/stats"
  ack_topic_template="gateway/+/ack"
  config_topic_template="gateway/{{ .MAC }}/config"

注意 他们都用了 +  通配符,也就是说,订阅了任意 Mac的 rx 和 status 主题

其中 func (b *MQTTBackend) rxPacketHandler(c mqtt.Client, msg mqtt.Message) 处理来自 uplink的主题

func (b *MQTTBackend) statsPacketHandler(c mqtt.Client, msg mqtt.Message) 处理来自 status的主题

GatewayBackend 没有显式的新增goroutine,当有相关主题消息被发布的时候,rxPacketHandler 和 statsPacketHandler就会被调用。我猜,应该是跑在MQTT 新建的 goroutine中。

rxPacketHandler()  负责接收mqtt发过来的主题推送,处理后丢给b.rxPacketChan管道。(文章后续会提及,G2负责一直读取这个管道)

rxPacketHandler() 源码如下:

func (b *MQTTBackend) rxPacketHandler(c mqtt.Client, msg mqtt.Message) {
	b.wg.Add(1)
	defer b.wg.Done()

	log.Info("backend/gateway: rx packet received")

	var phy lorawan.PHYPayload
	var rxPacketBytes gw.RXPacketBytes
	if err := json.Unmarshal(msg.Payload(), &rxPacketBytes); err != nil {
		log.WithFields(log.Fields{
			"data_base64": base64.StdEncoding.EncodeToString(msg.Payload()),
		}).Errorf("backend/gateway: unmarshal rx packet error: %s", err)
		return
	}

	if err := phy.UnmarshalBinary(rxPacketBytes.PHYPayload); err != nil {
		log.WithFields(log.Fields{
			"data_base64": base64.StdEncoding.EncodeToString(msg.Payload()),
		}).Errorf("backend/gateway: unmarshal phypayload error: %s", err)
	}

	// Since with MQTT all subscribers will receive the uplink messages sent
	// by all the gatewyas, the first instance receiving the message must lock it,
	// so that other instances can ignore the same message (from the same gw).
	// As an unique id, the gw mac + base64 encoded payload is used. This is because
	// we can't trust any of the data, as the MIC hasn't been validated yet.
	strB, err := phy.MarshalText()
	if err != nil {
		log.Errorf("backend/gateway: marshal text error: %s", err)
	}
	key := fmt.Sprintf("lora:ns:uplink:lock:%s:%s", rxPacketBytes.RXInfo.MAC, string(strB))
	redisConn := b.redisPool.Get()
	defer redisConn.Close()

	_, err = redis.String(redisConn.Do("SET", key, "lock", "PX", int64(uplinkLockTTL/time.Millisecond), "NX"))
	if err != nil {
		if err == redis.ErrNil {
			// the payload is already being processed by an other instance
			return
		}
		log.Errorf("backend/gateway: acquire uplink payload lock error: %s", err)
		return
	}

	b.rxPacketChan <- gw.RXPacket{
		RXInfo:     rxPacketBytes.RXInfo,
		PHYPayload: phy,
	}
}

先把mqtt broker发过来的msg,解析成 lorawan.PHYPayload 结构

然后看这段 注释

    // Since with MQTT all subscribers will receive the uplink messages sent
    // by all the gatewyas, the first instance receiving the message must lock it,
    // so that other instances can ignore the same message (from the same gw).
    // As an unique id, the gw mac + base64 encoded payload is used. This is because
    // we can't trust any of the data, as the MIC hasn't been validated yet.

根据注释,代码做了防护性设计,如果有多个backend订阅了 mqtt broker的 uplink_topic_template 主题,那么每一个订阅的backend都会收到并执行一次 rxPacketHandler(),为了把冗余的 主题内容剔除掉,作者借助了 redis 的分布式锁 。原理很简单,我把收到包的 gateway mac 和 内容 组成一个唯一key值,存到redis,如果redis告诉我已经有这个key了,那说明我这个包已经有人在处理了,我就直接返回了。如果这个key不存在,那么我就把这个包 推送到  b.rxPacketChan 中以待后续处理。key 自带了存活时间,超时后自动删除,防止无限增长。

这里我有个疑问,读代码的时候,GatewayBackend 似乎只初始化了一次啊,那么也就一个订阅了啊,哪来的多次 被调用啊。

你看服务器日志,

INFO[0000] backend/gateway: connected to mqtt server    
INFO[0000] backend/gateway: subscribing to rx topic      qos=0 topic=gateway/+/rx
INFO[0000] backend/gateway: subscribing to stats topic   qos=0 topic=gateway/+/stats

就一次啊,会不会作者被 冗余包搞怕了,反正写了顶多浪费点性能,也没啥大害处 :)

INFO[0000] setup redis connection pool                   url="redis://localhost:6379"
INFO[0000] connecting to postgresql                     
INFO[0000] backend/gateway: TLS config is empty         
INFO[0000] backend/gateway: connecting to mqtt broker    server="tcp://localhost:1883"
INFO[0000] configuring join-server client                ca_cert= server="http://localhost:8003" tls_cert= tls_key=
INFO[0000] no network-controller configured             
INFO[0000] applying database migrations                 
INFO[0000] backend/gateway: connected to mqtt server    
INFO[0000] backend/gateway: subscribing to rx topic      qos=0 topic=gateway/+/rx
INFO[0000] backend/gateway: subscribing to stats topic   qos=0 topic=gateway/+/stats
INFO[0000] migrations applied                            count=0
INFO[0000] starting api server                           bind="0.0.0.0:8000" ca-cert= tls-cert= tls-key=
INFO[0000] starting downlink device-queue scheduler     

GatewayBackend 对外提供的接口如下:

// Gateway is the interface of a gateway backend.
// A gateway backend is responsible for the communication with the gateway.
type Gateway interface {
	SendTXPacket(gw.TXPacket) error                       // send the given packet to the gateway
	SendGatewayConfigPacket(gw.GatewayConfigPacket) error // SendGatewayConfigPacket sends the given GatewayConfigPacket to the gateway.
	RXPacketChan() chan gw.RXPacket                       // channel containing the received packets
	StatsPacketChan() chan gw.GatewayStatsPacket          // channel containing the received gateway stats
	Close() error                                         // close the gateway backend.
}

GatewayBackend  通过MQTT Broker与 bridge 交互,  下发 lora-app-server 的命令,接收bridge的上传报文,join请求等

参考  loraserver 源码解析 (四) lora-gateway-bridgebridge.G4发布的

 

runDatabaseMigrations

run 完成了程序初始化后, 首先执行  runDatabaseMigrations 完成 PostgreSQL 数据库的初始化。

作者把需要在 postgresql 数据库创建的表格,索引等全部的SQL文件写到一起

github.com/brocaar/loraserver/migrations $ ls
0001_initial.sql                 0008_device_queue_emit_at_gps_ts.sql
0002_node_frame_log.sql          0009_gateway_profile.sql
0003_gateway_channel_config.sql  0010_device_skip_fcnt.sql
0004_update_gateway_model.sql    0011_cleanup_old_tables.sql
0005_profiles.sql                0012_lorawan_11.sql
0006_device_queue.sql            0013_cleanup_indices.sql
0007_routing_profile_certs.sql   0014_remove_gateway_name_and_descr.sql

然后利用 go-bindate

//go:generate go-bindata -prefix ../../migrations/ -pkg migrations -o ../../internal/migrations/migrations_gen.go ../../migrations/

把上面所有的 sql 文件转换成 migrations_gen.go 源码, 每个文件生成一个go数组

然后借助 github.com/rubenv/sql-migrate 完成PostgreSQL的初始化

func runDatabaseMigrations() error {
	if config.C.PostgreSQL.Automigrate {
		log.Info("applying database migrations")
		m := &migrate.AssetMigrationSource{
			Asset:    migrations.Asset,
			AssetDir: migrations.AssetDir,
			Dir:      "",
		}
		n, err := migrate.Exec(config.C.PostgreSQL.DB.DB.DB, "postgres", m, migrate.Up)
		if err != nil {
			return errors.Wrap(err, "applying migrations failed")
		}
		log.WithField("count", n).Info("migrations applied")
	}
	return nil
}

可参考  go-bindata 和 sql-migrate 用法

 

startAPIServer

数据库初始化后,开始启动 grpc 服务

grpc 不熟悉的同学可参考  go grpc protobuf 安装,然后再搜索下 grpc 相关用法

这里开启第一个 goroutine 以下简称G1

go gs.Serve(ln)

根据 grpc 源码  G1 每收到一个 tcp 连接,就会新开启一个 goroutine 处理,保持G1不阻塞

(稍后补充 grpc 接口 分析)

一般而言 G1 提供的 grpc 接口是给 lora-app-server 提供服务的

 

startLoRaServer

开启第二个goroutine 以下简称G2

G2 不断读取  GatewayBackend.RXPacketChan() 管道的 gw.RXPacket 报文,每收到一个就开启一个新的goroutine G2G处理,以保证G2不阻塞

这份开源的loraserver 把 LoRaWan network-server 拆分成2个部分,Bridge 和 loraserver。 Bridge负责和LoRaWan Gateway直接交互。由于LoRa Wan 终端的报文是以广播的形式发出的,附近的LoRaWan Gateway 收到后都会给 LoRaWan network-server 推送。比如附近有3个GateWay, GA,GB,GC。  那么当终端报文广播上传时,GA,GB,GC都收到了这个报文,然后通过udp上传给了Bridge。那么Bridge实际上就收到了3份一样的报文。

loraserver专门写了 collectAndCallOnce()  函数来处理多份相同报文。collectAndCallOnce 把这些相同的报文搜集起来,然后按信号强度排序,信号越强越前面。然后交给 collectAndCallOnce 回调,回调只会被调用一次。

比如终端上传了报文 p1,  附近的3个Gateway  GA GB GC 收到后上传给Bridge的报文分别是 GAP GBP  GCP,Bridge收到后,经mqtt传递给了 loraserver 的GatewayBackend GatewayBackend丢给了RXPacketChan()管道。G2读取这个管道的报文开启G2G 来处理。GAP GBP GCP 先后到达(到达顺序不一定,但到达时间差不多)。于是会有3个G2G分别处理GAP GBP GCP。G2G交给collectAndCallOnce()函数来处理相同报文。

由于是相同的报文, GAP GBP GCP中, payload是完全一样的。但是和Gateway相关的传输信息稍有不同。于是拿出payload组成 "lora:ns:rx:collect:payloadBase64" key值,在redis上构建成set,GAP GBP GCP报文则采用encoding/gob编码成buf存到这个set中。 于是3个G2G搞完后,redis上这个set "lora:ns:rx:collect:payloadBase64"   上会有3个成员

sadd set "lora:ns:rx:collect:payloadBase64"  encoding/gob(GAP) encoding/gob(GBP) encoding/gob(GCP)

const (
	CollectKeyTempl     = "lora:ns:rx:collect:%s"
	CollectLockKeyTempl = "lora:ns:rx:collect:%s:lock"
)

为了达到仅调用一次回调的目的,通过payload生成 CollectLockKeyTempl  lockkey,只有第一个获取这个 redis lockkey的 G2G 才需要调用回调,其余的redis告知lockkey已存在的G2G啥也不做,直接返回。

获取到lockkey的G2G会等待 time.Sleep(config.C.NetworkServer.DeduplicationDelay) 一段时间,这段时间内到达的相同报文会被搜集起来,如果GAP GBP GCP 到达时间间隔超过了这个范围,collectAndCallOnce() 就失效了,必须避免这种情况。


// collectAndCallOnce collects the package, sleeps the configured duraction and
// calls the callback only once with a slice of packets, sorted by signal
// strength (strongest at index 0). This method exists since multiple gateways
// are able to receive the same packet, but the packet needs to processed
// only once.
// It is safe to collect the same packet received by the same gateway twice.
// Since the underlying storage type is a set, the result will always be a
// unique set per gateway MAC and packet MIC.
func collectAndCallOnce(p *redis.Pool, rxPacket gw.RXPacket, callback func(packet models.RXPacket) error) error {
	var buf bytes.Buffer
	enc := gob.NewEncoder(&buf)
	if err := enc.Encode(rxPacket); err != nil {
		return fmt.Errorf("encode rx packet error: %s", err)
	}
	c := p.Get()
	defer c.Close()

	// store the packet in a set with DeduplicationDelay expiration
	// in case the packet is received by multiple gateways, the set will contain
	// each packet.
	// The text representation of the PHYPayload is used as key.
	phyB, err := rxPacket.PHYPayload.MarshalText()
	if err != nil {
		return errors.Wrap(err, "marshal to text error")
	}

	key := fmt.Sprintf(CollectKeyTempl, string(phyB))
	lockKey := fmt.Sprintf(CollectLockKeyTempl, string(phyB))

	// this way we can set a really low DeduplicationDelay for testing, without
	// the risk that the set already expired in redis on read
	deduplicationTTL := config.C.NetworkServer.DeduplicationDelay * 2
	if deduplicationTTL < time.Millisecond*200 {
		deduplicationTTL = time.Millisecond * 200
	}

	c.Send("MULTI")
	c.Send("SADD", key, buf.Bytes())
	c.Send("PEXPIRE", key, int64(deduplicationTTL)/int64(time.Millisecond))
	_, err = c.Do("EXEC")
	if err != nil {
		return fmt.Errorf("add rx packet to collect set error: %s", err)
	}

	// acquire a lock on processing this packet
	_, err = redis.String(c.Do("SET", lockKey, "lock", "PX", int64(deduplicationTTL)/int64(time.Millisecond), "NX"))
	if err != nil {
		if err == redis.ErrNil {
			// the packet processing is already locked by an other process
			// so there is nothing to do anymore :-)
			return nil
		}
		return fmt.Errorf("acquire lock error: %s", err)
	}

	// wait the configured amount of time, more packets might be received
	// from other gateways
	time.Sleep(config.C.NetworkServer.DeduplicationDelay)

	// collect all packets from the set
	payloads, err := redis.ByteSlices(c.Do("SMEMBERS", key))
	if err != nil {
		return fmt.Errorf("get collect set members error: %s", err)
	}
	if len(payloads) == 0 {
		return errors.New("zero items in collect set")
	}

	var out models.RXPacket
	for _, b := range payloads {
		var packet gw.RXPacket
		if err := gob.NewDecoder(bytes.NewReader(b)).Decode(&packet); err != nil {
			return errors.Wrap(err, "decode rx packet error")
		}

		out.PHYPayload = packet.PHYPayload
		out.TXInfo = models.TXInfo{
			Frequency: packet.RXInfo.Frequency,
			DataRate:  packet.RXInfo.DataRate,
			CodeRate:  packet.RXInfo.CodeRate,
		}

		out.RXInfoSet = append(out.RXInfoSet, models.RXInfo{
			MAC:               packet.RXInfo.MAC,
			Time:              packet.RXInfo.Time,
			TimeSinceGPSEpoch: packet.RXInfo.TimeSinceGPSEpoch,
			Timestamp:         packet.RXInfo.Timestamp,
			RSSI:              packet.RXInfo.RSSI,
			LoRaSNR:           packet.RXInfo.LoRaSNR,
			Board:             packet.RXInfo.Board,
			Antenna:           packet.RXInfo.Antenna,
		})
	}

	sort.Sort(out.RXInfoSet)
	return callback(out)
}

接下来看看回调函数的处理,根据报文类型,分别执行 join,  rejoin, data 或者 proprietary

		uplinkFrameSet, err := framelog.CreateUplinkFrameSet(rxPacket)
		if err != nil {
			return errors.Wrap(err, "create uplink frame-set error")
		}

		if err := framelog.LogUplinkFrameForGateways(uplinkFrameSet); err != nil {
			log.WithError(err).Error("log uplink frames for gateways error")
		}

		switch rxPacket.PHYPayload.MHDR.MType {
		case lorawan.JoinRequest:
			return join.Handle(rxPacket)
		case lorawan.RejoinRequest:
			return rejoin.Handle(rxPacket)
		case lorawan.UnconfirmedDataUp, lorawan.ConfirmedDataUp:
			return data.Handle(rxPacket)
		case lorawan.Proprietary:
			return proprietary.Handle(rxPacket)
		default:
			return nil
		}

其中 LogUplinkFrameForGateways() 函数把 uplinkFrameSet 一条条的通过 redis 发布订阅。主题 "lora:ns:gw:%s:pubsub:frame:uplink" %s用 Gateway 的 mac代替。

loraserver 的 ns grpc 接口的 StreamFrameLogsForGateway 对 这个 redis 主题进行订阅。

lora-app-server 通过grpc 调用 StreamFrameLogsForGateway 后,StreamFrameLogsForGateway会开启一个goroutine,一直读取 redis gateway 主题, 于是一条条的上传报文就 通过  redis 转交给了 lora-app-server

最终显示成类似下面这样的页面

 

join.Handle 

如果上传的报文是 JoinRequest 类型的,就进行 OTAA(在线激活) 处理。 OTAA的代码集中在 github.com/brocaar/loraserver/internal/uplink/join 的 join.go 中,以后用空详细分析下,此处不再展开。大概的意思是 LoRa 终端设备入网方式有2种,一种是 ABP,预先设置好 设备地址 appkey networkkey 这些信息。 还有一种是 OTAA, 设备入网时先发送 JoinRequst 报文, loraserver 处理后返回JoinReponse,动态生成设备的 地址 appkey 等信息。

rejoin.Handle

如果 上传的是 RejoinRequest 则进行  rejoin 处理,是上面 OTAA 的一部分,我暂时也不太清楚,以后有机会补上详细的。

data.Handle

若是 lorawan.UnconfirmedDataUp, lorawan.ConfirmedDataUp 类型的报文,则继续推送。它们都是终端上传数据的报文。区别在于Confirmed 的 loraserver 收到报文后会发送一条 downlink 报文以表明收到数据了。

根据devaddr 获取存在  redis 的device session ( device session 里存了很多信息,设备相关的eui啊,network key 啊 fcnt 啊等等),校验fcnt, mic 如果都没有问题,pub一个 redis  主题  deviceFrameLogUplinkPubSubKeyTempl    = "lora:ns:device:%s:pubsub:frame:uplink"  %s由 devie session存的devEUI 替代。注意这里是 设备的 主题,lora-app-server 点开设备的日志页面,便会通过loraserver grpc 订阅这个主题,然后一条条消息经grpc 的 stream 传送到网页上给我们查看。

之后获取  DeviceProfile ,DeviceProfile存在 PostgreSQL 中,并在 redis 中做了个带存活时间的缓存。

再获取一些其它信息后 视 报文情况 给予反馈报文

 

终端的同一份报文 如果被多个 gateway 上传给了loraserver, loraserver会选出收到包的信号强度最高的。

然后根据这个的data-rate, 调整出合适的 txPower

adr.go 的 HandleADR 处理 根据 daterate 调节 txPower
先根据接收到数据包的 DataRate 获取存在 band 中的 DataRate
然后根据 dr.SpreadFactor 计算出需要的 SNR(信噪比)
snrM 最大SNR    requiredSNR 刚计算出来的 需要的SNR
snrMargin := snrM - requiredSNR - config.C.NetworkServer.NetworkSettings.InstallationMargin
nStep := int(snrMargin / 3)

getIdealTXPowerOffsetAndDR
根据 nStep 和 原先的 TXPowerIndex 调整出 最终的 TXPowerIndexadr 

 

startStatsServer

通过 gwStats.Start() 开启开启第三个主goroutine 以下简称 G3

G3 不断读取 GatewayBackend 的StatsPacketChan管道

对于每个到达的数据报文都开启一个新的goroutine (以下简称G3G) 来处理。

回想前面提到的GatewayBackend 启动后订阅了  bridge任意 Mac的 rx 和 status 主题

收到status主题发布的数据后,statsPacketHandler被调用,statsPacketHandler解析出 statsPacket 丢给 StatsPacketChan管道

G3G交给 storage.HandleGatewayStatsPacket 来处理具体的 status 信息,把gateway修改后的信息保存到postgresql数据库中

 

startQueueScheduler

开启第四个主goroutine 以下简称 G4

执行 downlink 设备队列调度,每隔一段时间执行一个c或b类型设备调度

依次看看各设备有没有没发送的命令,有的话打包下,发送出去

根据loraserver源码,  G4一遍遍死循环调用ScheduleBatch,  ScheduleBatch 借助 storage包封装好的Transaction函数,把具体工作都交到了事务中完成。 

// Transaction wraps the given function in a transaction. In case the given
// functions returns an error, the transaction will be rolled back.
func Transaction(db *common.DBLogger, f func(tx sqlx.Ext) error) error {
	tx, err := db.Beginx()
	if err != nil {
		return errors.Wrap(err, "begin transaction error")
	}

	err = f(tx)
	if err != nil {
		if rbErr := tx.Rollback(); rbErr != nil {
			return errors.Wrap(rbErr, "transaction rollback error")
		}
		return err
	}

	if err := tx.Commit(); err != nil {
		return errors.Wrap(err, "transaction commit error")
	}
	return nil
}

// ScheduleBatch schedules a downlink batch.
func ScheduleBatch(size int) error {
	return storage.Transaction(config.C.PostgreSQL.DB, func(tx sqlx.Ext) error {
		devices, err := storage.GetDevicesWithClassBOrClassCDeviceQueueItems(tx, size)
		if err != nil {
			return errors.Wrap(err, "get deveuis with class-c device-queue items error")
		}

		for _, d := range devices {
			ds, err := storage.GetDeviceSession(config.C.Redis.Pool, d.DevEUI)
			if err != nil {
				log.WithError(err).WithField("dev_eui", d.DevEUI).Error("get device-session error")
				continue
			}

			err = data.HandleScheduleNextQueueItem(ds)
			if err != nil {
				log.WithError(err).WithField("dev_eui", d.DevEUI).Error("schedule next device-queue item error")
			}
		}

		return nil
	})
}

// GetDevicesWithClassBOrClassCDeviceQueueItems returns a slice of devices that qualify
// for downlink Class-C transmission.
// The device records will be locked for update so that multiple instances can
// run this query in parallel without the risk of duplicate scheduling.
func GetDevicesWithClassBOrClassCDeviceQueueItems(db sqlx.Ext, count int) ([]Device, error) {
	gpsEpochScheduleTime := gps.Time(time.Now().Add(config.ClassCScheduleInterval * 2)).TimeSinceGPSEpoch()

	var devices []Device
	err := sqlx.Select(db, &devices, `
        select
            d.*
        from
            device d
        inner join device_profile dp
            on dp.device_profile_id = d.device_profile_id
        where (
            	dp.supports_class_c = true
            	or dp.supports_class_b = true
            )
            -- we want devices with queue items
            and exists (
                select
                    1
                from
                    device_queue dq
                where
                    dq.dev_eui = d.dev_eui
                    and (
                    	dp.supports_class_c = true
                    	or (
                    		dp.supports_class_b = true
                    		and dq.emit_at_time_since_gps_epoch <= $2
                    	)
                    )
            )
            -- we don't want device with pending queue items that did not yet
            -- timeout
            and not exists (
                select
                    1
                from
                    device_queue dq
                where
                    dq.dev_eui = d.dev_eui
                    and is_pending = true
                    and dq.timeout_after > now()
            )
        order by
            d.dev_eui
        limit $1
        for update of d skip locked`,
		count,
		gpsEpochScheduleTime,
	)
	if err != nil {
		return nil, handlePSQLError(err, "select error")
	}

	return devices, nil
}

仔细研究了源码,我发现仅GetDevicesWithClassBOrClassCDeviceQueueItems函数使用了 sqlx.Beginx() 返回的连接 tx,其余的sql语句都没有使用这个tx。因此仅 GetDevicesWithClassBOrClassCDeviceQueueItems() 的 那句 sql 语句包含在了事务中。其余的  HandleScheduleNextQueueItem() 中包含的大量sql 语句由于没有使用这个tx , 都不属于这个事务。 这是作者有意为之,还是疏忽了?    

GetDevicesWithClassBOrClassCDeviceQueueItems() 中 sql 结尾处 for update of d skip locked 我不太熟悉,查阅相关资料后整理如下:

   依照《postgresql-11-A4.pdf》13.3.2, for update 是表格的行锁,锁定表格里的行,锁由强到弱共有4种
   1. FOR UPDATE    2. FOR NO KEY UPDATE  3. FOR SHARE 4. FOR KEY SHARE
   以下为 go 操作 postgresql 默认情况下行为
   BEGIN;  开始事务
   select * from student;  这里select 语句直接执行,并不会等到commit后才执行
   此处如果做了耗时的工作,比如休眠3秒,其它连接是可以修改student表格的数据的
   select * from student;  再次查询后,值可能和上次不一样
   
   select * from student for update; 注意这句带了锁, 相关的行就会被锁住不让修改
   如果这里花费了一些时间做耗时的操作  那么其它连接若对 student 修改将阻塞直到 commit;
   commit; 提交事务
   其它连接 如果仅仅  select * from student; 而没有带 for update;则不会阻塞

   假设有2个上述事务同时运行,它们前面的sql语句都会顺利执行,直到 select * from student for update; 这一行
   优先获得锁的将继续执行,没获得锁的将等待直到前者commit
   锁住期间,其它修改性操作将阻塞

   skip locked 可以让事务进入锁住的行,我搜索了 loraserver 源码,仅一个 goroutine 用到了这个事务,因此这个句话在目前源码中应该没有发挥作用。

   sqlx.Beginx() 开启事务并返回一个单独的连接,本质上而言事务就是让这个返回的连接把一系列sql串起来执行。
   因此,Beginx() 后,如果没有使用其返回值执行sql,而是用其它的连接执行sql,那么此sql将不认为是事务的一部分。

评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值