通过FIFO完全连接到MySQL客户端

5
在Bash脚本中,我希望能够在多个连续访问之间保持MySQL会话的开放状态。通常访问MySQL的方式是为每个SQL命令或一组命令打开单独的会话,例如:
mysql -u user -e "show tables;"

这种方法的局限性在于会丢失需要进行两次操作的事务的原子性和锁定状态:例如,在整个双重操作的过程中无法保留表格T的锁定状态。
### Minimalistic example
data=$(mysql -e "\
    lock table T write;
    select col from T;
")
# ...
# parse 'data' and compute 'output' variable
# ...
mysql -e "insert into T values ($output);"

我的解决方案是通过使用两个FIFO并将进程挂在后台,保持MySQL会话跨多个访问保持打开状态。


建议的解决方案:
创建一对FIFO: mkfifo IN OUT.
将MySQL客户端实例放置在适当位置,并带有一个虚拟的while循环以保持管道打开并防止SIGPIPE信号:

mysql --xml --batch --raw --skip-column-names \
    -h "$hostname" -u "$username" "$db" >IN <OUT &
while :; do sleep 1; done               <IN >OUT &

然后进行测试:
echo "show tables;" >OUT
read <IN

结果:
它不起作用。 echo 命令完成并且 bash 跳过它,这意味着 MySQL 接收了输入,但是 read 永远挂起,因此没有输出产生。
我发现消除 IN FIFO 整个任务就不会挂起:

mysql --xml --batch --raw --skip-column-names \
    -h "$hostname" -u "$username" "$db" <OUT &
while :; do sleep 1; done               >OUT &

echo "show tables;" >OUT  # this produces the expected output

这种行为是否符合预期?我还想知道在Bash中是否可能进行双重操作而无需自定义homebrews。


你尝试使用追加重定向(>>)或管道符号(|)了吗?好问题,祝你好运! - shellter
如果你不必使用shell脚本,并且已经安装了PHP,为什么不创建一个包装器shell来创建到已建立连接的管道命令。你可以从这里开始:http://squirrelshaterobots.com/programming/php/building-a-queue-server-in-php-part-3-accepting-input-from-named-pipes/ - crafter
我曾经在一些bash脚本中遇到了同样的问题。有一段时间,管理起来非常不方便。后来我发现了Python,并选择它作为我的编程语言。它有一个用于与MySQL交互的模块。 - glglgl
4个回答

7

FIFO的问题在于当输入数据的每个进程终止时,它会向正在读取数据的进程(在本例中为mysql)发出终止信号,从而导致其终止。

关键是确保始终有一个进程保持FIFO输入活动。您可以通过在后台运行sleep 999999999 > fifofile来实现这一点。

示例:

#!/bin/sh

mkfifo /tmp/sqlpipe

sleep 2147483647 > /tmp/sqlpipe &
PID=$!

mysql -B -uUSER -pPASSWORD < /tmp/sqlpipe &

# all set up, now just push the SQL queries to the pipe, exemple:
echo "INSERT INTO table VALUES (...);" > /tmp/sqlpipe
echo "INSERT INTO table VALUES (...);" > /tmp/sqlpipe
echo "INSERT INTO table VALUES (...);" > /tmp/sqlpipe
cat "mysqldump.sql" > /tmp/sqlpipe
echo "DELETE FROM table WHERE ...;" > /tmp/sqlpipe

# done! terminate pipe
kill -s SIGINT $PID
rm /tmp/sqlpipe

最后,我们终止sleep进程来完全释放FIFO输入。这将向mysql发出信号说明输入已结束,其将自动死亡。
还有另一种不需要FIFO的替代方案,但您需要两个脚本: run.sh:
#!/bin/sh
./querygenerator.sh | mysql -B -uUSER -pPASSWORD

querygenerator.sh:

#!/bin/sh
echo "INSERT INTO table VALUES (...);"
echo "INSERT INTO table VALUES (...);"
echo "INSERT INTO table VALUES (...);"
cat "mysqldump.sql"
echo "DELETE FROM table WHERE ...;"

1

我之前开发了一个作弊程序来解决这种问题,使用了unix socketpair()。它只在脚本运行时持续存在(即使在后台),但比FIFO更容易处理。

socketpair()调用在指定的域中创建一个未命名的已连接套接字对,使用指定的类型和可选的协议。

下面是一个完整的示例,包括二进制文件的源代码。不要被吓倒,你可以在交互式shell中轻松地尝试这个想法:

local:/# ./socketpair /bin/bash
$ cat <& $DUP1 | tr '[:lower:]' '[:upper:]' &
$ echo 'Hello SocketPair!' >& $DUP2
HELLO SOCKETPAIR!
$

这是一个简单的脚本:

#!./socketpair /usr/bin/env bash
# We are now in a BASH script with a pair of linked sockets,
# $DUP1 and $DUP2

## Background job ## Received data on DUP1
(
    while read -r -u $DUP1
    do
        echo "Received: $REPLY"
    done 
) &


## Foreground task ## Sends data to DUP2
counter=0
while true
do
    echo Test $(( counter++ )) >&$DUP2
    sleep 1
done

在脚本中包含源代码,以防将来移动脚本并且无法找到二进制文件 :)
## Source code for simple 'socketpair' binary
## Compile with "cc -o socketpair socketpair.c"
: <<'SOURCE'
--[ cut here ]--
/** 
* @file socketpair.c
* @author christopher anserson
* @date 2012-04-28
*/
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>

char* custom_itoa(int i) {
    static char output[24];
     return sprintf(output, "%d", i), output;
}

int main(int argc, char **argv) {
    int sv[2]; /* the pair of socket descriptors */

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == -1) {
        perror("socketpair");
        exit(1);
    }
     setenv("DUP1", custom_itoa(sv[0]), 1);
     setenv("DUP2", custom_itoa(sv[1]), 1);

     /* now exec whatever script needed these paired sockets */
     execv(argv[1], &argv[1]);
    return 0;
}
--[cut here]--
SOURCE

0

我知道这个帖子很旧了,但我也在寻找一个舒适的bash mysql会话实现,没有找到足够满意的,所以我写了自己的实现,并想与大家分享。

############### BASIC MYSQL SESSION IMPLEMENTATION FOR BASH (by Norman 

Geist 2015) #############
# requires coproc, stdbuf, mysql
#args: handle query
function mysql_check {
  local handle
  handle=(${1//_/ })
  #has right structure && is still running && we opened it?
  if [[ ${#handle[*]} == 3 ]] && ps -p ${handle[2]} 2>> /dev/null >> /dev/null && { echo "" >&${handle[1]}; } 2> /dev/null; then
    return 0
  fi
  return 1
}

# open mysql connection
#args: -u user [-H host] [-p passwd] -d db
#returns $HANDLE
function mysql_connect {
  local argv argc user pass host db HANDLEID i
  #prepare args
  argv=($*)
  argc=${#argv[*]}

  #get options
  user=""
  pass=""
  host="localhost"
  db=""
  for ((i=0; $i < $argc; i++))
  do
    if [[ ${argv[$i]} == "-h" ]]; then
      echo "Usage: -u user [-H host] [-p passwd] -d db"
      return 0
    elif [[ ${argv[$i]} == "-u" ]]; then
      i=$[$i+1]
      if [[ ${#argv[$i]} -gt 0 ]]; then
    user=${argv[$i]}
      else
    echo "ERROR: -u expects argument!"
    return 1
      fi
    elif [[ ${argv[$i]} == "-p" ]]; then
      i=$[$i+1]
      if [[ ${#argv[$i]} -gt 0 ]]; then
    pass="-p"${argv[$i]}
      else
    echo "ERROR: -p expects argument!"
    return 1
      fi
    elif [[ ${argv[$i]} == "-H" ]]; then
      i=$[$i+1]
      if [[ ${#argv[$i]} -gt 0 ]]; then
    host=${argv[$i]}
      else
    echo "ERROR: -H expects argument!"
    return 1
      fi
    elif [[ ${argv[$i]} == "-d" ]]; then
      i=$[$i+1]
      if [[ ${#argv[$i]} -gt 0 ]]; then
    db=${argv[$i]}
      else
    echo "ERROR: -d expects argument!"
    return 1
      fi
    fi
  done

  if [[ ${#user} -lt 1 || ${#db} -lt 1 ]]; then
    echo "ERROR: Options -u user and -d db are required!"
    return 1;
  fi

  #init connection and channels
  #we do it in XML cause otherwise we can't detect the end of data and so would need a read timeout O_o
  HANDLEID="MYSQL$RANDOM"
  eval "coproc $HANDLEID { stdbuf -oL mysql -u $user $pass -h $host -D $db --force --unbuffered --xml -vvv 2>&1; }" 2> /dev/null
  HANDLE=$(eval 'echo ${'${HANDLEID}'[0]}_${'${HANDLEID}'[1]}_${'${HANDLEID}'_PID}')
  if mysql_check $HANDLE; then
    export HANDLE
    return 0
  else
    echo "ERROR: Connection failed to $user@$host->DB:$db!"
    return 1
  fi
}

#args: handle query
#return: $DATA[0] = affected rows/number of sets; 
#        $DATA[1] = key=>values pairs following
#        $DATA[2]key; DATA[3]=val ...
function mysql_query {
  local handle query affected line results_open row_open cols key val 
  if ! mysql_check $1; then
    echo "ERROR: Connection not open!"
    return 1
  fi
  handle=(${1//_/ })

  #delimit query; otherwise we block forever/timeout
  query=$2
  if [[ ! "$query" =~ \;\$ ]]; then
    query="$query;"
  fi
  #send query
  echo "$query" >&${handle[1]}

  #get output
  DATA=();
  DATA[0]=0
  DATA[1]=0
  results_open=0
  row_open=0
  cols=0
  while read -t $MYSQL_READ_TIMEOUT -ru ${handle[0]} line
  do 
    #WAS ERROR?
    if [[ "$line" == *"ERROR"* ]]; then
      echo "$line"
      return 1
    #WAS INSERT/UPDATE?
    elif [[ "$line" == *"Query OK"* ]]; then
      affected=$([[ "$line" =~ Query\ OK\,\ ([0-9]+)\ rows?\ affected ]] && echo ${BASH_REMATCH[1]})
      DATA[0]=$affected
      export DATA
      return 0
    fi

    #BEGIN OF RESULTS
    if [[ $line =~ \<resultset ]]; then
      results_open=1
    fi

    #RESULTS
    if [[ $results_open == 1 ]]; then
      if [[ $line =~ \<row ]]; then
    row_open=1
    cols=0
      elif [[ $line =~ \<field && $row_open == 1 ]]; then
    key=$([[ "$line" =~ name\=\"([^\"]+)\" ]] && echo ${BASH_REMATCH[1]})
    val=$([[ "$line" =~ \>(.*)\<\/ ]] && echo ${BASH_REMATCH[1]} || echo "NULL")
    DATA[${#DATA[*]}]=$key
    DATA[${#DATA[*]}]=$val
    cols=$[$cols+1]
      elif [[ $line =~ \<\/row ]]; then
    row_open=0
    DATA[0]=$[${DATA[0]}+1]
    DATA[1]=$cols
      fi
    fi

    #END OF RESULTS
    if [[ $line =~ \<\/resultset ]]; then
      export DATA
      return 0
    fi
  done
  #we can only get here
  #if read times out O_o
  echo "$FUNCNAME: Read timed out!"
  return 1
}

#args: handle
function mysql_close {
  local handle
  if ! mysql_check $1; then
    echo "ERROR: Connection not open!"
    return 1
  fi
  handle=(${1//_/ })
  echo "exit;" >&${handle[1]}

  if ! mysql_check $1; then
    return 0
  else
    echo "ERROR: Couldn't close connection!"
    return 1
  fi
}
############### END BASIC MYSQL SESSION IMPLEMENTATION FOR BASH ################################

# Example usage
#define timeout for read command, in case of server error etc.
export MYSQL_READ_TIMEOUT=10

# Connect to db and get $HANDLE
mysql_connect -u mydbuser -d mydb -H mydbserver

#query db and get $DATA
mysql_query $HANDLE "SELECT dt_whatever from tbl_lol WHERE dt_rofl=10"

#close connection
mysql_close $HANDLE

注意:

  • 在连接后,将$HANDLE保存到一个新变量中,以便打开尽可能多的连接
  • 不能在bash会话之间交换$HANDLE
  • 您需要安装Linux软件包"coproc"、"stdbuf"和"mysql"
  • 返回的数据是一个bash数组
$DATA[0] = affected rows/number of sets;
$DATA[1] = number of key=>values pairs following;
$DATA[2] = key1;
$DATA[3] = value1;
      [...]
$DATA[n-1] = keyn;
$DATA[n]   = valuen;
通常所有的查询都应该有效,甚至包括"SELECT count(*)"。

以下是如何循环遍历返回的两列查询数据的示例

例如:"SELECT dt_id, dt_name FROM ..."

fields=2
for ((i=2; $i<$((${DATA[0]}*${DATA[1]}*$fields)); i+=$((${DATA[1]}*$fields))))
do
    field1key   = ${DATA[$i]};   #this is "dt_id"
    field1value = ${DATA[$i+1]}; #this is the value for dt_id
    field2key   = ${DATA[$i+2]}; #this is "dt_name"
    field2value = ${DATA[$i+3]}; #this is the value  for dt_name
done

0
这里有一个简单的示例,可以重现你所描述的死锁行为。
while :; do sleep 1; done <IN >OUT

sed s/^/::/ >IN <OUT

cat IN

echo x > OUT

echo 命令完成了,但是 catsed 没有完成。也许尝试一下这个例子会有一些结果。

虽然我不确定你需要以这种方式做,但也许最好的方法是编写一个控制脚本,将其管道传输到 MySQL,并让 MySQL 将其输出写入文件。

#!/bin/bash
set -o errexit -o nounset -o pipefail

# Setup variables, hostname, names of files...

{
  echo 'BEGIN PROGRAM' >&2
  cat <<MYSQL
    # Do something to set the output file, maybe \t?
    lock table T write;
    select col from T;
    # Run a shell command to create a mysql.done file from MySQL
MYSQL
  while [[ ! -f mysql.done ]]
  do sleep 1
  done
  # Create rows to insert from MySQL output file.
  echo 'load data infile input.tsv into table T'
} | mysql --xml --batch --raw --skip-column-names -h "$hostname" -u "$username" "$db"

通过FIFO控制流比在while循环中等待文件出现更具吸引力;但是,FIFO却出奇地难以正确使用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接