題の通り。

ロジックとしては

 

■モデル
class AppModel extends Model {
    function lockTableRW() {
        $this->lockTable(READ);
        $this->lockTable(WRITE);
    }

    function lockTable($type=READ) {
        $dbo = $this->getDataSource();
        $dbo->execute(LOCK TABLES .$this->table. .$type);
    }

    function unlockTables() {
        $dbo = $this->getDataSource();
        $dbo->execute(UNLOCK TABLES);
    }
}

■コントローラ

    public function test() {
        App::uses(MySampleData, Model);
        // ロック
        $model = new MySampleData();
        $model->lockTable(WRITE);
 
        // * ここでデッドロックが起こる
        $semiRandomArticle = $model->find(first);
        exit();
    }
 
ちなみに$model->lockTable(READ);でも起こる。
吐かれているSQLを見た。
($this->[Model]->getDataSource()->getLog())
 
-------
LOCK TABLES my_sample_data  WRITE;
 
SELECT `MySampleData`.`id`, `MySampleData`.`name`, `MySampleData`.`mail`, `MySampleData`.`tel` FROM `cake`.`my_sample_data` AS `MySampleData`   WHERE 1 = 1    LIMIT 1
-------
 
MySQLでは
「LOCK TABLES の使用時には、使用するテーブルをすべてロックし、
  またクエリで使用するエイリアスと同じ名前を使用する必要があります。
  1 つのクエリで同じテーブルを何度も指定する(エイリアスを使用して)
  場合は、各エイリアスに対してロックを取得しなければなりません。」
http://www.mysql.gr.jp/mysqlml/mysql/msg/11217
 
つまりCakeがfindする際にMySampleDataというエイリアスをつけているので、
それをロックできていなくて不具合が起きた。
 
---------
何年か前に、おそらく同じ原因でCakeの2系でデッドロックが発生したことがあった。
そのときは自分で調査を行ったり、人に聞いても結局問題は解決せず、別の方法で問題を回避した。
プロジェクトが終わった後もこの件がずっと気になっていて、いつか原因を突き止めようと思っていた。
これだけ長引いてしまったのは不具合を再現する環境を構築するのが面倒だったのと、優先順位を下げてしまっていたため。
記憶を頼りにしたコードであるため、断言できないけれども、おそらくこんな感じだったはず。
 
当時問題を解決できなかった理由は、
調査する術を知らなかったことと(直接吐かれるSQLを見なかった。見方を知らなかったのか、発想すら浮かばなかったのかは覚えていない)、
FWの奥の方に書かれているため、FW側が用意したメソッドだと思っていた(FWが用意したメソッドなら、十中八九使い方を誤っているのだろうと思った)。
FWにしても100%バグがないわけではないから、今にして思うとこれも誤りである。
 
最近になって調べてみるとCakeのリファレンスのどこにもそのようなメソッドは見つからなかった(どこかのサイトが引っかかったので、それを参照したと思われる)。
今でも覚えてるのはgrepしても、ロックしている箇所が一つもなかったので、もしかしたらろくにテストされていなかったのかも。
 
調べれば調べるほど、CakePHP嫌いになっていくんだよな。
複合キーもサポートしないし。なんで流行ってるのかわからない
(日本語の資料が多いからと言われたことがあるが、なぜ多いのだろう)。
今は3系が出ているけど、今はfuelやlaravelも出ているし、あえて調べ直す気にもならない。